提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
hello大家好久没见,之所以隔了这么久才更新并不是因为我又放弃了这个项目,而是接下来要制作的工作太忙碌了,每次我都花了很长的时间解决完一个部分,然后就没力气打开CSDN写文章就直接睡觉去了,现在终于有时间整理下我这半个月都做了什么内容
废话少说,接下来我将介绍我做的几个代表性场景,主要是如标题说的,制作更多地图,更多敌人,更多可交互对象
另外,我的Github已经更新了,想要查看最新的内容话请到我的Github主页下载工程吧:
GitHub - ForestDango/Hollow-Knight-Demo: A new Hollow Knight Demo after 2 years!
一、第一个代表性场景
1.制作更多敌人
我们先把制作好创建吧,还是老规矩先用tk2dTilemap绘制好基础的地图样貌并添加上Collider:
然后堆叠素材添加上去,这样一个场景就做好了:
首先当然是从Town跳下井里的第一个场景Crossroads_01,这里我们设置Town到Crossroads_01的TransitionPoint为top2:
然后回到Crossroads中我们设置好全部的TransitionPoint,这个top1别管:
然后我们添加上敌人,其实这里敌人没什么要讲的因为都是我们之前就做过的,直接预制体拖上去就完事了
主要是要修复这个Zombie的bug,我们之前做的Walker.cs脚本有问题,现在让我们修复bug,问题在EndStopping中:我就说之前怎么会莫名其妙的转身,
cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Walker : MonoBehaviour
{
[Header("Structure")]
//检测玩家的脚本一个不能少
[SerializeField] private LineOfSightDetector lineOfSightDetector;
[SerializeField] private AlertRange alertRange;
//每一个敌人的四件公式化挂载rb2d,col2d,animator,audiosource,再加一个摄像头和hero位置
private Rigidbody2D body;
private Collider2D bodyCollider;
private tk2dSpriteAnimator animator;
private AudioSource audioSource;
private Camera mainCamera;
private HeroController hero;
private const float CameraDistanceForActivation = 60f;
private const float WaitHeroXThreshold = 1f; //距离玩家X方向上的极限距离值
[Header("Configuration")]
[SerializeField] private bool ambush; //是否埋伏
[SerializeField] private string idleClip; //idle的动画片段名字
[SerializeField] private string turnClip; //turn的动画片段名字
[SerializeField] private string walkClip; //walk的动画片段名字
[SerializeField] private float edgeXAdjuster; //检测墙沿x上的增加值
[SerializeField] private bool preventScaleChange; //是否防止x轴的localscale发生变化
[SerializeField] private bool preventTurn; //是否阻止转向
[SerializeField] private float pauseTimeMin; //停止不动的时间
[SerializeField] private float pauseTimeMax;
[SerializeField] private float pauseWaitMin; //走路的时间
[SerializeField] private float pauseWaitMax;
[SerializeField] private bool pauses; //是否需要静止状态
[SerializeField] private float rightScale; //开始时的x轴方向
[SerializeField] public bool startInactive; //开始时不活跃
[SerializeField] private int turnAfterIdlePercentage; //Idle状态过后进入转身Turn状态的概率
[SerializeField] private float turnPause; //设置转身的冷却时间
[SerializeField] private bool waitForHeroX; //是否等待玩家X方向到位
[SerializeField] private float waitHeroX; //等待玩家X方向距离
[SerializeField] public float walkSpeedL; //向左走路的速度
[SerializeField] public float walkSpeedR;//向右走路的速度
[SerializeField] public bool ignoreHoles; //是否忽略洞
[SerializeField] private bool preventTurningToFaceHero; //防止转向玩家的位置
[SerializeField] private Walker.States state;
[SerializeField] private Walker.StopReasons stopReason;
private bool didFulfilCameraDistanceCondition; //暂时没有用到
private bool didFulfilHeroXCondition; //暂时没有用到
private int currentFacing;//Debug的时候可以在前面加个[SerializeField]
private int turningFacing;
//三个计时器且顾名思义
private float walkTimeRemaining;
private float pauseTimeRemaining;
private float turnCooldownRemaining;
protected void Awake()
{
body = GetComponent<Rigidbody2D>();
bodyCollider = GetComponent<BoxCollider2D>();
animator = GetComponent<tk2dSpriteAnimator>();
audioSource = GetComponent<AudioSource>();
}
protected void Start()
{
mainCamera = Camera.main;
hero = HeroController.instance;
if(currentFacing == 0)
{
currentFacing = ((transform.localScale.x * rightScale >= 0f) ? 1 : -1); //左边是-1,右边是1
}
if(state == States.NotReady)
{
turnCooldownRemaining = -Mathf.Epsilon;
BeginWaitingForConditions();
}
}
/// <summary>
/// 我们创建另一个状态机,分为四种状态,每一种都有Update和Stop的方法。
/// </summary>
protected void Update()
{
turnCooldownRemaining -= Time.deltaTime;
switch (state)
{
case States.WaitingForConditions:
UpdateWaitingForConditions();
break;
case States.Stopped:
UpdateStopping();
break;
case States.Walking:
UpdateWalking();
break;
case States.Turning:
UpdateTurning();
break;
default:
break;
}
}
/// <summary>
/// 从Waiting状态进入开始移动状态(不一定是Walk也可能是Turn)
/// </summary>
public void StartMoving()
{
if(state == States.Stopped || state == States.WaitingForConditions)
{
startInactive = false;
int facing;
if(currentFacing == 0)
{
facing = UnityEngine.Random.Range(0, 2) == 0 ? -1 : 1;
}
else
{
facing = currentFacing;
}
BeginWalkingOrTurning(facing);
}
Update();
}
/// <summary>
/// 在需要时取消转向
/// </summary>
public void CancelTurn()
{
if(state == States.Turning)
{
BeginWalking(currentFacing);
}
}
public void Go(int facing)
{
turnCooldownRemaining = -Time.deltaTime;
if(state == States.Stopped || state == States.Walking)
{
BeginWalkingOrTurning(facing);
}
else if(state == States.Turning && currentFacing == facing)
{
CancelTurn();
}
Update();
}
public void ReceiveGoMessage(int facing) //TODO:
{
if(state != States.Stopped || stopReason != StopReasons.Controlled)
{
Go(facing);
}
}
/// <summary>
/// 被脚本StopWalker.cs调用,更改reason为controlled
/// </summary>
/// <param name="reason"></param>
public void Stop(StopReasons reason)
{
BeginStopped(reason);
}
/// <summary>
/// 更改turningFacing和currentFacing,属于Turn状态的行为
/// </summary>
/// <param name="facing"></param>
public void ChangeFacing(int facing)
{
if(state == States.Turning)
{
turningFacing = facing;
currentFacing = -facing;
return;
}
currentFacing = facing;
}
/// <summary>
/// 开始进入等待状态
/// </summary>
private void BeginWaitingForConditions()
{
state = States.WaitingForConditions;
didFulfilCameraDistanceCondition = false;
didFulfilHeroXCondition = false;
UpdateWaitingForConditions();
}
/// <summary>
/// 在Update以及BeginWaitingForConditions两大函数中调用,更新等待状态下的行为
/// </summary>
private void UpdateWaitingForConditions()
{
if (!didFulfilCameraDistanceCondition && (mainCamera.transform.position - transform.position).sqrMagnitude < CameraDistanceForActivation * CameraDistanceForActivation)
{
didFulfilCameraDistanceCondition = true;
}
if(didFulfilCameraDistanceCondition && !didFulfilHeroXCondition && hero != null &&
Mathf.Abs(hero.transform.position.x - waitHeroX) < WaitHeroXThreshold) //TODO:
{
didFulfilHeroXCondition = true;
}
if(didFulfilCameraDistanceCondition && (!waitForHeroX || didFulfilHeroXCondition) && !startInactive && !ambush)
{
BeginStopped(StopReasons.Bored);
StartMoving();
}
}
/// <summary>
/// 开始进入停止状态
/// </summary>
/// <param name="reason"></param>
private void BeginStopped(StopReasons reason)
{
state = States.Stopped;
stopReason = reason;
if (audioSource)
{
audioSource.Stop();
}
if(reason == StopReasons.Bored)
{
tk2dSpriteAnimationClip clipByName = animator.GetClipByName(idleClip);
if(clipByName != null)
{
animator.Play(clipByName);
}
body.velocity = Vector2.Scale(body.velocity, new Vector2(0f, 1f)); //相当于把x方向上的速度设置为0
if (pauses)
{
pauseTimeRemaining = UnityEngine.Random.Range(pauseTimeMin, pauseTimeMax);
return;
}
EndStopping();
}
}
/// <summary>
/// 在Update中被调用,执行停止Stop状态的行为
/// </summary>
private void UpdateStopping()
{
if(stopReason == StopReasons.Bored)
{
pauseTimeRemaining -= Time.deltaTime;
if(pauseTimeRemaining <= 0f)
{
EndStopping();
}
}
}
/// <summary>
/// 终止停止状态
/// </summary>
private void EndStopping()
{
if(currentFacing == 0)
{
BeginWalkingOrTurning(UnityEngine.Random.Range(0, 2) == 0 ? 1 : -1);
return;
}
if(UnityEngine.Random.Range(0,100) < turnAfterIdlePercentage)
{
BeginTurning(-currentFacing);
return;
}
BeginWalking(currentFacing); //这里应该是开始行走Walk而不是开始转向Turn
}
/// <summary>
/// 要不走路要不转身
/// </summary>
/// <param name="facing"></param>
private void BeginWalkingOrTurning(int facing)
{
if(currentFacing == facing)
{
BeginWalking(facing);
return;
}
BeginTurning(facing);
}
/// <summary>
/// 开始进入Walking状态
/// </summary>
/// <param name="facing"></param>
private void BeginWalking(int facing)
{
state = States.Walking;
animator.Play(walkClip);
if (!preventScaleChange)
{
transform.SetScaleX(facing * rightScale);
}
walkTimeRemaining = UnityEngine.Random.Range(pauseWaitMin, pauseWaitMax);
if (audioSource)
{
audioSource.Play();
}
body.velocity = new Vector2((facing > 0) ? walkSpeedR : walkSpeedL,body.velocity.y);
}
/// <summary>
/// 在Update中被调用,动态执行Walking状态,根据情况决定是否要进入Turning状态或者Stopped状态
/// </summary>
private void UpdateWalking()
{
if(turnCooldownRemaining <= 0f)
{
Sweep sweep = new Sweep(bodyCollider, 1 - currentFacing, Sweep.DefaultRayCount,Sweep.DefaultSkinThickness);
if (sweep.Check(transform.position, bodyCollider.bounds.extents.x + 0.5f, LayerMask.GetMask("Terrain")))
{
BeginTurning(-currentFacing);
return;
}
if (!preventTurningToFaceHero && (hero != null && hero.transform.GetPositionX() > transform.GetPositionX() != currentFacing > 0) && lineOfSightDetector != null && lineOfSightDetector.CanSeeHero && alertRange != null && alertRange.IsHeroInRange)
{
BeginTurning(-currentFacing);
return;
}
if (!ignoreHoles)
{
Sweep sweep2 = new Sweep(bodyCollider, DirectionUtils.Down, Sweep.DefaultRayCount, 0.1f);
if (!sweep2.Check((Vector2)transform.position + new Vector2((bodyCollider.bounds.extents.x + 0.5f + edgeXAdjuster) * currentFacing, 0f), 0.25f, LayerMask.GetMask("Terrain")))
{
BeginTurning(-currentFacing);
return;
}
}
}
if (pauses)
{
walkTimeRemaining -= Time.deltaTime;
if(walkTimeRemaining <= 0f)
{
BeginStopped(StopReasons.Bored);
return;
}
}
body.velocity = new Vector2((currentFacing > 0) ? walkSpeedR : walkSpeedL, body.velocity.y);
}
private void BeginTurning(int facing)
{
state = States.Turning;
turningFacing = facing;
if (preventTurn)
{
EndTurning();
return;
}
turnCooldownRemaining = turnPause;
body.velocity = Vector2.Scale(body.velocity, new Vector2(0f, 1f));
animator.Play(turnClip);
FSMUtility.SendEventToGameObject(gameObject, (facing > 0) ? "TURN RIGHT" : "TURN LEFT", false);
}
/// <summary>
/// 在Update中被调用,执行Turning转身状态。
/// </summary>
private void UpdateTurning()
{
body.velocity = Vector2.Scale(body.velocity, new Vector2(0f, 1f));
if (!animator.Playing)
{
EndTurning();
}
}
/// <summary>
/// 被UpdateTurning()调用,当动画播放完成后切换到Walking状态。
/// 被BeginTurning()调用,当preventTurn为true时就不再向下执行了。
/// </summary>
private void EndTurning()
{
currentFacing = turningFacing;
BeginWalking(currentFacing);
}
/// <summary>
/// 就清空turnCooldownRemaining
/// </summary>
public void ClearTurnCoolDown()
{
turnCooldownRemaining = -Mathf.Epsilon;
}
public enum States
{
NotReady,
WaitingForConditions,
Stopped,
Walking,
Turning
}
public enum StopReasons
{
Bored,
Controlled
}
}
然后我们在面板中重新设置好参数:
2.制作更多可交互对象
这个场景里可交互的貌似只有这个杆,我们添加好它的顶部和底部并设置好位置,添加上layer然后新建脚本:BreakablePole.cs
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BreakablePole : MonoBehaviour,IHitResponder
{
[SerializeField] private SpriteRenderer spriteRenderer;
[SerializeField] private Sprite brokenSprite;
[SerializeField] private float inertBackgroundThreshold;
[SerializeField] private float inertForegroundThreshold;
[SerializeField] private AudioSource audioSourcePrefab;
[SerializeField] private RandomAudioClipTable hitClip;
[SerializeField] private GameObject slashImpactPrefab;
[SerializeField] private Rigidbody2D top;
protected void Reset()
{
inertBackgroundThreshold = -1f;
inertForegroundThreshold = -1f;
}
protected void Start()
{
float z = transform.position.z;
if(z < inertBackgroundThreshold || z > inertForegroundThreshold)
{
enabled = false;
return;
}
}
public void Hit(HitInstance damageInstance)
{
int cardinalDirection = DirectionUtils.GetCardinalDirection(damageInstance.Direction);
if (cardinalDirection != 2 && cardinalDirection != 0)
{
return;
}
spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.g, spriteRenderer.color.b,0f);
Transform transform = Instantiate(slashImpactPrefab).transform;
transform.eulerAngles = new Vector3(0f, 0f, Random.Range(340f, 380f));
Vector3 localScale = transform.localScale;
localScale.x = ((cardinalDirection == 2) ? -1f : 1f);
localScale.y = 1f;
hitClip.SpawnAndPlayOneShot(audioSourcePrefab, base.transform.position);
if (top != null)
{
top.gameObject.SetActive(true);
float num = (cardinalDirection == 2) ? Random.Range(120, 140) : Random.Range(40, 60);
top.transform.localScale = new Vector3(localScale.x, localScale.y, top.transform.localScale.z);
top.velocity = new Vector2(Mathf.Cos(num * 0.017453292f), Mathf.Sin(num * 0.017453292f)) * 5f;
top.transform.Rotate(new Vector3(0f, 0f, num));
base.enabled = false;
}
}
}
然后设置好参数:
二、第二个代表性场景
1.制作更多敌人
然后就是大家喜闻乐见的长场景Crossroads_07,这个场景相当于一个区域的中转站,既可以向左走去虫爷爷和苍绿之境,或者打boss躁郁的毛里克,亦可以向右走去打假骑士和苍蝇之母,鹿角站,矿井等等,不过这些都是后话了,我们先来把地图做好,添加上对应的TranstionPoint:
其实做到这里我才意识到要把这四个 TranstionPoint做成预制体。
然后敌人自然是到处飞的苍蝇fly了:
2.制作可交互对象
为了让场景看起来生动,我们可以添加背景板里飞走的蚊子buzzer,用particlesystem来实现
第二个:
然后就是踩一下会发出声音的平台:
我们来给它们新建一个脚本LiftPlatform.cs:实现了个功能当角色踩上去时上下移动一下,播放粒子系统和声音:
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LiftPlatform : MonoBehaviour
{
public GameObject part1;
public GameObject part2;
public ParticleSystem dustParticle;
public AudioSource source;
private float part1_start_y;
private float part2_start_y;
private int state;
private float timer;
private void Start()
{
part1_start_y = part1.transform.position.y;
part2_start_y = part2.transform.position.y;
}
private void Update()
{
if(state == 1)
{
if (timer < 0.125f)
{
part1.transform.position = new Vector3(part1.transform.position.x, part1_start_y - timer * 0.75f, part1.transform.position.z);
part2.transform.position = new Vector3(part2.transform.position.x, part2_start_y - timer * 0.75f, part2.transform.position.z);
timer += Time.deltaTime;
}
else
{
part1.transform.position = new Vector3(part1.transform.position.x, part1_start_y - 0.09f, part1.transform.position.z);
part2.transform.position = new Vector3(part2.transform.position.x, part2_start_y - 0.09f, part2.transform.position.z);
state = 2;
timer = 0.12f;
}
}
if(state == 2)
{
if (timer > 0f)
{
part1.transform.position = new Vector3(part1.transform.position.x, part1_start_y - timer * 0.75f, part1.transform.position.z);
part2.transform.position = new Vector3(part2.transform.position.x, part2_start_y - timer * 0.75f, part2.transform.position.z);
timer -= Time.deltaTime;
return;
}
part1.transform.position = new Vector3(part1.transform.position.x, part1_start_y, part1.transform.position.z);
part2.transform.position = new Vector3(part2.transform.position.x, part2_start_y, part2.transform.position.z);
state = 0;
}
}
private void OnCollisionEnter2D(Collision2D collision)
{
if(state == 0 && collision.collider.gameObject.layer != LayerMask.NameToLayer("Item") && collision.gameObject.layer != LayerMask.NameToLayer("Particle") && collision.gameObject.layer != LayerMask.NameToLayer("Enemies") && collision.GetSafeContact().Normal.y < 0.1f)
{
source.pitch = Random.Range(0.85f, 1.15f);
source.Play();
dustParticle.Play();
state = 0;
timer = 0f;
}
}
}
三、第三个代表性场景
1.制作更多的敌人
其实这个场景我就想将新制作的敌人的:没错就是这个拿骨钉盾牌的僵尸虫
我们回到tk2dSprite和tk2danimator给它创建好:
你只需要记住,A是anticipate攻击准备阶段的动画,L是Lunge突刺动画,S是攻击时的动画,CD是冷却时候的动画, 然后剩下的dddd。这个Bump就是反弹骨钉攻击的动画。OK说的差不多了,然后Unsheild和sheild动画就是反过来的。
然后就是添加相应的脚本到场景中:
s上面的脚本我在之前的文章都讲过了,除了这个EnemyDeathEffects:
cs
using System;
using HutongGames.PlayMaker;
using UnityEngine;
using UnityEngine.Audio;
public class EnemyDeathEffects : MonoBehaviour
{
[SerializeField] private GameObject corpsePrefab;
[SerializeField] private bool corpseFacesRight;
[SerializeField] private float corpseFlingSpeed;
[SerializeField] public Vector3 corpseSpawnPoint;
[SerializeField] private string deathBroadcastEvent;
[SerializeField] private Vector3 effectOrigin;
[SerializeField] private bool lowCorpseArc;
[SerializeField] private EnemyDeathTypes enemyDeathType;
[SerializeField] protected AudioSource audioPlayerPrefab;
[SerializeField] protected AudioEvent enemyDeathSwordAudio;
[SerializeField] protected AudioEvent enemyDamageAudio;
[SerializeField] protected AudioClip enemyDeathSwordClip;
[SerializeField] protected AudioClip enemyDamageClip;
[SerializeField] private AudioMixerSnapshot audioSnapshotOnDeath;
[SerializeField] protected GameObject deathWaveInfectedPrefab;
[SerializeField] protected GameObject deathWaveInfectedSmallPrefab;
[SerializeField] private bool recycle;
[SerializeField] private bool rotateCorpse; //尸体需要旋转吗
[SerializeField] protected GameObject dustPuffMedPrefab;
[SerializeField] protected GameObject deathPuffLargePrefab;
protected GameObject corpse;
private bool didFire;
[HideInInspector]
public bool doKillFreeze = true;
protected void Start()
{
PreInstantiate();
}
public void PreInstantiate()
{
if(!corpse && corpsePrefab)
{
corpse = Instantiate(corpsePrefab, transform.position + corpseSpawnPoint, Quaternion.identity, transform);
tk2dSprite[] componentInChildrens = corpse.GetComponentsInChildren<tk2dSprite>(true);
for (int i = 0; i < componentInChildrens.Length; i++)
{
componentInChildrens[i].ForceBuild();
}
corpse.SetActive(false);
}
}
public void RecieveDeathEvent(float? attackDirection, bool resetDeathEvent = false, bool spellBurn = false, bool isWatery = false)
{
if (didFire)
return;
didFire = true;
if(corpse != null)
{
EmitCorpse(attackDirection, isWatery, spellBurn);
}
if (!isWatery)
{
EmitEffects();
}
if (doKillFreeze)
{
GameManager.instance.FreezeMoment(1);
}
if (enemyDeathType == EnemyDeathTypes.Infected || enemyDeathType == EnemyDeathTypes.LargeInfected || enemyDeathType == EnemyDeathTypes.SmallInfected || enemyDeathType == EnemyDeathTypes.Uninfected )
{
EmitEssence();
}
if (audioSnapshotOnDeath != null)
{
audioSnapshotOnDeath.TransitionTo(2f);
}
if (!string.IsNullOrEmpty(deathBroadcastEvent))
{
Debug.LogWarningFormat(this, "Death broadcast event '{0}' not implemented!", new object[]
{
deathBroadcastEvent
});
}
if (resetDeathEvent)
{
FSMUtility.SendEventToGameObject(gameObject, "CENTIPEDE DEATH", false);
didFire = false;
return;
}
if (recycle)
{
PlayMakerFSM playMakerFSM = FSMUtility.LocateFSM(gameObject, "health_manager_enemy");
if(playMakerFSM != null)
{
playMakerFSM.FsmVariables.GetFsmBool("Activated").Value = false;
}
HealthManager component2 = GetComponent<HealthManager>();
if(component2 != null)
{
component2.SetIsDead(false);
}
didFire = false;
//TODO:
return;
}
Destroy(gameObject);
}
private void EmitCorpse(float? attackDirection, bool isWatery, bool spellBurn)
{
if (corpse == null)
return;
corpse.transform.SetParent(null);
corpse.transform.SetPositionZ(UnityEngine.Random.Range(-0.08f, -0.09f));
corpse.SetActive(true);
PlayMakerFSM playMakerFSM = FSMUtility.LocateFSM(corpse, "corpse");
if(playMakerFSM != null)
{
FsmBool fsmBool = playMakerFSM.FsmVariables.GetFsmBool("spellBurn");
if(fsmBool!= null)
{
fsmBool.Value = false;
}
}
Corpse component = corpse.GetComponent<Corpse>();
if (component)
{
component.Setup(isWatery, spellBurn);
}
if (isWatery)
{
return;
}
corpse.transform.SetRotation2D(rotateCorpse ? transform.GetRotation2D():0f);
if(Mathf.Abs(transform.eulerAngles.z) >= 45f)
{
Collider2D component2 = GetComponent<Collider2D>();
Collider2D component3 = corpse.GetComponent<Collider2D>();
if(!rotateCorpse && component2 && component3)
{
Vector3 b = component2.bounds.center - component3.bounds.center;
b.z = 0f;
corpse.transform.position += b;
}
}
float d = 1f;
if(attackDirection == null)
{
d = 0f;
}
int cardinalDirection = DirectionUtils.GetCardinalDirection(attackDirection.GetValueOrDefault());
Rigidbody2D component4 = corpse.GetComponent<Rigidbody2D>();
if(component4 != null && !component4.isKinematic)
{
float num = corpseFlingSpeed;
float num2;
switch (cardinalDirection)
{
case 0:
num2 = lowCorpseArc ? 10f : 60f;
corpse.transform.SetScaleX(corpse.transform.localScale.x * (corpseFacesRight ? -1f : 1f) * Mathf.Sign(transform.localScale.x));
break;
case 1:
num2 = UnityEngine.Random.Range(75f, 105f);
num *= 1.3f;
break;
case 2:
num2 = lowCorpseArc ? 170f : 120f;
corpse.transform.SetScaleX(corpse.transform.localScale.x * (corpseFacesRight ? 1f : -1f) * Mathf.Sign(transform.localScale.x));
break;
case 3:
num2 = 270f;
break;
default:
num2 = 90f;
break;
}
component4.velocity = new Vector2(Mathf.Cos(num2 * 0.017453292f), Mathf.Sin(num2 * 0.017453292f)) * num * d;
}
}
private void EmitEffects()
{
EnemyDeathTypes enemyDeathTypes = enemyDeathType;
if(enemyDeathTypes == EnemyDeathTypes.Infected)
{
EmitInfectedEffects();
return;
}
if (enemyDeathTypes == EnemyDeathTypes.SmallInfected)
{
EmitSmallInfectedEffects();
return;
}
if (enemyDeathTypes != EnemyDeathTypes.LargeInfected)
{
Debug.LogWarningFormat(this, "Enemy death type {0} not implemented!", new object[]
{
enemyDeathType
});
return;
}
EmitLargeInfectedEffects();
}
private void EmitLargeInfectedEffects()
{
AudioEvent audioEvent = default(AudioEvent);
audioEvent.Clip = enemyDeathSwordClip;
audioEvent.PitchMin = 0.75f;
audioEvent.PitchMax = 0.75f;
audioEvent.Volume = 1f;
audioEvent.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);
audioEvent = default(AudioEvent);
audioEvent.Clip = enemyDamageClip;
audioEvent.PitchMin = 0.75f;
audioEvent.PitchMax = 0.75f;
audioEvent.Volume = 1f;
audioEvent.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);
if(corpse != null)
{
SpriteFlash component = corpse.GetComponent<SpriteFlash>();
if(component != null)
{
component.flashInfected();
}
}
if (!(deathPuffLargePrefab == null))
{
Instantiate(deathPuffLargePrefab, transform.position + effectOrigin, Quaternion.identity);
}
ShakeCameraIfVisible("AverageShake");
if (!(deathWaveInfectedPrefab == null))
{
GameObject gameObject = Instantiate(deathWaveInfectedPrefab, transform.position + effectOrigin, Quaternion.identity);
gameObject.transform.SetScaleX(2f);
gameObject.transform.SetScaleY(2f);
}
GlobalPrefabDefaults.Instance.SpawnBlood(transform.position + effectOrigin, 75, 80, 20f, 25f, 0f, 360f, null);
}
private void EmitSmallInfectedEffects()
{
AudioEvent audioEvent = default(AudioEvent);
audioEvent.Clip = enemyDeathSwordClip;
audioEvent.PitchMin = 1.2f;
audioEvent.PitchMax = 1.4f;
audioEvent.Volume = 1f;
audioEvent.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);
audioEvent = default(AudioEvent);
audioEvent.Clip = enemyDamageClip;
audioEvent.PitchMin = 1.2f;
audioEvent.PitchMax = 1.4f;
audioEvent.Volume = 1f;
audioEvent.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);
if (deathWaveInfectedSmallPrefab != null)
{
GameObject gameObject = Instantiate(deathWaveInfectedSmallPrefab, transform.position + effectOrigin,Quaternion.identity);
Vector3 localScale = gameObject.transform.localScale;
localScale.x = 0.5f;
localScale.y = 0.5f;
gameObject.transform.localScale = localScale;
}
GlobalPrefabDefaults.Instance.SpawnBlood(transform.position + effectOrigin, 8, 10, 15f, 20f, 0, 360, null);
}
private void EmitInfectedEffects()
{
EmitSound();
if(corpse != null)
{
SpriteFlash component = corpse.GetComponent<SpriteFlash>();
if(component != null)
{
component.flashInfected();
}
}
GameObject gameObject = Instantiate(deathWaveInfectedPrefab, transform.position + effectOrigin, Quaternion.identity);
gameObject.transform.SetScaleX(1.25f);
gameObject.transform.SetPositionY(1.25f);
GlobalPrefabDefaults.Instance.SpawnBlood(transform.position + effectOrigin, 8, 10, 15f, 20f, 0, 360, null);
Instantiate(dustPuffMedPrefab, transform.position + effectOrigin, Quaternion.identity);
ShakeCameraIfVisible("EnemyKillShake");
}
private void EmitSound()
{
enemyDeathSwordAudio.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);
enemyDamageAudio.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);
}
private void EmitEssence()
{
//TODO:和梦之钉有关的
PlayerData playerData = GameManager.instance.playerData;
if (!playerData.hasDreamNail)
{
return;
}
}
protected void ShakeCameraIfVisible(string eventName)
{
Renderer renderer = GetComponent<Renderer>();
if (renderer == null)
{
renderer = GetComponentInChildren<Renderer>();
}
if (renderer != null && renderer.isVisible)
{
GameCameras.instance.cameraShakeFSM.SendEvent(eventName);
}
}
}
在面板中添加好参数后,我们就到HealthManager中:
cs
public class HealthManager : MonoBehaviour, IHitResponder
{
private EnemyDeathEffects enemyDeathEffects;
protected void Awake()
{
enemyDeathEffects = GetComponent<EnemyDeathEffects>();
}
public void Die(float? attackDirection, AttackTypes attackType, bool ignoreEvasion)
{
if (isDead)
{
return;
}
if (sprite)
{
sprite.color = Color.white;
}
FSMUtility.SendEventToGameObject(gameObject, "ZERO HP", false);
if (hasSpecialDeath)
{
NonFatalHit(ignoreEvasion);
return;
}
isDead = true;
if(damageHero != null)
{
damageHero.damageDealt = 0;
}
if(battleScene != null && !notifiedBattleScene)
{
PlayMakerFSM playMakerFSM = FSMUtility.LocateFSM(battleScene, "Battle Control");
if(playMakerFSM != null)
{
FsmInt fsmInt = playMakerFSM.FsmVariables.GetFsmInt("Battle Enemies");
if(fsmInt != null)
{
fsmInt.Value--;
notifiedBattleScene = true;
}
}
}
if (enemyDeathEffects != null)
{
if (attackType == AttackTypes.Generic)
{
enemyDeathEffects.doKillFreeze = false;
}
enemyDeathEffects.RecieveDeathEvent(attackDirection, deathReset, attackType == AttackTypes.Spell, false);
}
SendDeathEvent();
Destroy(gameObject); //TODO:
}
}
还有我们将蚊子那期介绍的老朋友攻击距离检测,警戒距离检测
还有类似于玩家的slash的polygon collider2d,别忘了给它们添加上damagehero脚本
还有一个就是当僵尸虫进入冲刺状态上播放的粒子系统dust:
接下来就到了我们老朋友playmakerFSM登场了,老规矩我先贴出来变量和事件然后逐个讲状态:
第一个状态当然是初始化了:
每一帧都检测玩家是否到可视范围和攻击范围了
判断玩家位置:
我们先做好playmakerFSM自定义脚本:
cs
using HutongGames.PlayMaker;
using UnityEngine;
[ActionCategory("Hollow Knight")]
public class SetInvincible : FsmStateAction
{
[UIHint(UIHint.Variable)]
public FsmOwnerDefault target;
public FsmBool Invincible;
public FsmInt InvincibleFromDirection;
public override void Reset()
{
target = new FsmOwnerDefault();
Invincible = null;
InvincibleFromDirection = null;
}
public override void OnEnter()
{
GameObject safe = target.GetSafe(this);
if (safe != null)
{
HealthManager component = safe.GetComponent<HealthManager>();
if (component != null)
{
if (!Invincible.IsNone)
{
component.IsInvincible = Invincible.Value;
}
if (!InvincibleFromDirection.IsNone)
{
component.InvincibleFromDirection = InvincibleFromDirection.Value;
}
}
}
Finish();
}
}
cs
using HutongGames.PlayMaker;
using UnityEngine;
[ActionCategory("Hollow Knight")]
public class SetWalkerFacing : WalkerAction
{
public FsmBool walkRight;
public FsmBool randomStartDir;
public override void Reset()
{
base.Reset();
walkRight = new FsmBool
{
UseVariable = true
};
randomStartDir = new FsmBool();
}
/// <summary>
/// 调用Walker.cs中的ChangeFacing函数来改变朝向
/// </summary>
/// <param name="walker"></param>
protected override void Apply(Walker walker)
{
if (randomStartDir.Value)
{
walker.ChangeFacing((Random.Range(0, 2) == 0) ? -1 : 1);
return;
}
if (!walkRight.IsNone)
{
walker.ChangeFacing(walkRight.Value ? 1 : -1);
}
}
}
剩下三个都差不多,你只需要设置好tk2d动画,Lung1 Speed和Lung2 Speed的正负,判断不同方向的事件,以及sheild格挡的方向:
触发BLOCKED HIT的事件该执行的状态:
这个BLOCKED HIT的事件在HealthManager.cs中会触发的,让我们回到HealthManager.cs脚本中:
cs
public void Invincible(HitInstance hitInstance)
{
int cardinalDirection = DirectionUtils.GetCardinalDirection(hitInstance.GetActualDirection(transform));
directionOfLastAttack = cardinalDirection;
FSMUtility.SendEventToGameObject(gameObject, "BLOCKED HIT", false);
FSMUtility.SendEventToGameObject(hitInstance.Source, "HIT LANDED", false);
if (!(GetComponent<DontClinkGates>() != null))
{
FSMUtility.SendEventToGameObject(gameObject, "HIT", false);
if(hitInstance.AttackType == AttackTypes.Nail)
{
if(cardinalDirection == 0)
{
HeroController.instance.RecoilLeft();
}
else if(cardinalDirection == 2)
{
HeroController.instance.RecoilRight();
}
}
Vector2 v;
Vector3 eulerAngles;
if (boxCollider != null)
{
switch (cardinalDirection)
{
case 0:
v = new Vector2(transform.GetPositionX() + boxCollider.offset.x - boxCollider.size.x * 0.5f, hitInstance.Source.transform.GetPositionY());
eulerAngles = new Vector3(0f, 0f, 0f);
break;
case 1:
v = new Vector2(hitInstance.Source.transform.GetPositionX(), Mathf.Max(hitInstance.Source.transform.GetPositionY(), transform.GetPositionY() + boxCollider.offset.y - boxCollider.size.y * 0.5f));
eulerAngles = new Vector3(0f, 0f, 90f);
break;
case 2:
v = new Vector2(transform.GetPositionX() + boxCollider.offset.x + boxCollider.size.x * 0.5f, hitInstance.Source.transform.GetPositionY());
eulerAngles = new Vector3(0f, 0f, 180f);
break;
case 3:
v = new Vector2(hitInstance.Source.transform.GetPositionX(), Mathf.Min(hitInstance.Source.transform.GetPositionY(), transform.GetPositionY() + boxCollider.offset.y + boxCollider.size.y * 0.5f));
eulerAngles = new Vector3(0f, 0f, 270f);
break;
default:
break;
}
}
else
{
v = transform.position;
eulerAngles = new Vector3(0f, 0f, 0f);
}
}
evasionByHitRemaining = 0.15f;
}
那么这个方法在哪里被调用呢,当然是我们的HIT方法了,如果你还有印象,这个就是继承接口IHitResponder要实现的方法:
cs
public void Hit(HitInstance hitInstance)
{
if (isDead)
{
return;
}
if(evasionByHitRemaining > 0f)
{
return;
}
if(hitInstance.DamageDealt < 0f)
{
return;
}
FSMUtility.SendEventToGameObject(hitInstance.Source, "DEALT DAMAGE", false);
int cardinalDirection = DirectionUtils.GetCardinalDirection(hitInstance.GetActualDirection(transform));
if (IsBlockingByDirection(cardinalDirection, hitInstance.AttackType))
{
Invincible(hitInstance);
return;
}
TakeDamage(hitInstance);
}
判断当前攻击方向是否格挡:
cs
[Header("Invincible")]
[SerializeField] private bool invincible;
[SerializeField] private int invincibleFromDirection;
public bool IsBlockingByDirection(int cardinalDirection,AttackTypes attackType)
{
//法术攻击无法格挡
if(attackType == AttackTypes.Spell && gameObject.CompareTag("Spell Vulnerable"))
{
return false;
}
//不是无敌无法格挡
if (!invincible)
{
return false;
}
//没有确切的方向无法格挡
if(invincibleFromDirection == 0)
{
return true;
}
switch (cardinalDirection)
{
case 0:
{
int num = invincibleFromDirection;
if (num <= 5)
{
if (num != 1 && num != 5)
{
return false;
}
}
else if (num != 8 && num != 10)
{
return false;
}
return true;
}
case 1:
{
int num = invincibleFromDirection;
return num == 2 || num - 5 <= 4;
}
case 2:
{
int num = invincibleFromDirection;
if (num <= 6)
{
if (num != 3 && num != 6)
{
return false;
}
}
else if (num != 9 && num != 11)
{
return false;
}
return true;
}
case 3:
{
int num = invincibleFromDirection;
return num == 4 || num - 7 <= 4;
}
default:
return false;
}
}
格挡以后自然是要对玩家发起攻击了:
首先进入准备阶段:
向前冲刺阶段:
攻击阶段,在这里就要打开我们创建的slash的碰撞箱了,同时将僵尸虫的速度位置为0
冷却阶段,关闭碰撞箱:
然后突然虚晃一枪接着对玩家发动二段攻击,没有准备阶段直接进入冲刺攻击阶段
二阶段的冷却:
三阶段的再次冲刺攻击阶段:
三阶段的攻击阶段:
三阶段的冷却阶段,应该不叫冷却而是叫停止攻击阶段:
重置walker状态:
除了自动攻击我们还有主动攻击阶段,内容我就不赘述了直接上图:
还有就是玩家离开可视范围和攻击距离发送LEFT RANGE事件,把举起的盾牌放下来
总结
我们来看看上述讲到的三个场景的效果(上面的UI先别管我之后会完成的):
虫子的转身没问题的
平台没问题的
然后僵尸盾牌虫:直接攻击Attack 1
它在反击后的攻击是水平方向的,因此我可以下劈它
由于我没做格挡时的动画,虽然不明显但是还是能看到敌人并没有因为我的攻击而受到伤害。。
三段攻击也有,但我忘了截屏了就先这样吧,下一期我们来制作更多的敌人和更多的场景