Unity开发2D类银河恶魔城游戏学习笔记
技能系统
Unity教程(二十一)技能系统 基础部分
Unity教程(二十二)技能系统 分身技能
Unity教程(二十三)技能系统 掷剑技能(上)基础实现
Unity教程(二十四)技能系统 掷剑技能(中)技能变种实现
Unity教程(二十五)技能系统 掷剑技能(下)冻结时间实现
如果你更习惯用知乎
Unity开发2D类银河恶魔城游戏学习笔记目录
文章目录
- Unity开发2D类银河恶魔城游戏学习笔记
- 前言
- 一、概述
- 二、冻结时间的实现
- [总结 完整代码](#总结 完整代码)
前言
注意:Udemy上更新了课程,原来的版本删掉了。新版教程与旧版顺序相差较大,并且改用Unity6。但这个笔记已经写到一半了,所以还是按照旧版来。
本文为Udemy课程The Ultimate Guide to Creating an RPG Game in Unity学习笔记,如有错误,欢迎指正。
本节实现角色投剑技能冻结时间的功能。
对应视频:
Bouncy sword
Setting sword type
Pierce sword
Saw spin sword
一、概述
本节给投剑技能添加冻结时间的功能。
在Enemy类中添加敌人冻结后的反馈,并创建协程。
设置时间冻结的持续时长,由Sword_Skill传入控制器。
在Sword_Skill_Controller中检测敌人,并在击中敌人时调用冻结敌人的协程实现时间冻结。

二、冻结时间的实现
首先在Enemy中添加敌人被冻结后应有的反应。敌人被冻结时,速度归零,动画停止播放;冻结结束后,恢复原速度,恢复动画播放速度。
添加变量 defaultMoveSpeed记录敌人原有的速度。
创建协程,实现将敌人冻结一段时间。
代码如下:
csharp
[Header("Move Info")]
public float moveSpeed = 1.5f;
public float idleTime = 2.0f;
public float battleTime = 4.0f;
private float defaultMoveSpeed;
protected override void Awake()
{
base.Awake();
stateMachine = new EnemyStateMachine();
defaultMoveSpeed = moveSpeed;
}
public virtual void FreezeTime(bool _timeFrozen)
{
if(_timeFrozen)
{
moveSpeed = 0;
anim.speed = 0;
}
else
{
moveSpeed = defaultMoveSpeed;
anim.speed = 1;
}
}
protected virtual IEnumerator FreezeTimeFor(float _seconds)
{
FreezeTime(true);
yield return new WaitForSeconds(_seconds);
FreezeTime(false);
}
在Sword_Skill中添加冻结时间持续时长,并传至控制器
csharp
[Header("Skill Info")]
[SerializeField] private GameObject swordPrefab;
[SerializeField] private Vector2 launchForce;
[SerializeField] private float swordGravity;
[SerializeField] private float returnSpeed;
[SerializeField] private float freezeTimeDuration;
public void CreateSword()
{
GameObject newSword = Instantiate(swordPrefab, player.transform.position, transform.rotation);
Sword_Skill_Controller newSwordScript = newSword.GetComponent<Sword_Skill_Controller>();
if (swordType == SwordType.Bounce)
newSwordScript.SetupBounce(true, bounceAmount, bounceSpeed);
else if (swordType == SwordType.Pierce)
newSwordScript.SetupPierce(pierceAmount);
else if (swordType== SwordType.Spin)
newSwordScript.SetupSpin(true, maxTravelDistance, spinDuration, hitCooldown);
newSwordScript.SetupSword(finalDir, swordGravity, player, returnSpeed,freezeTimeDuration);
player.AssignNewSword(newSword);
DotsActive(false);
}
在Sword_Skill_Controller中添加变量freezeTimeDuration,在SetupSword函数中接收传来的参数。
在触发器函数OnTriggerEnter2D中检测敌人,并调用协程FreezeTimeFor冻结敌人。
csharp
private float freezeTimeDuration;
public void SetupSword(Vector2 _dir, float _gravityScale, Player _player, float _returnSpeed, float _freezeTimeDuration)
{
player = _player;
rb.velocity = _dir;
rb.gravityScale = _gravityScale;
returnSpeed = _returnSpeed;
freezeTimeDuration = _freezeTimeDuration;
if (pierceAmount<=0)
anim.SetBool("Rotation", true);
Invoke("DestroyMe", 7.0f);
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (isReturning)
return;
if(collision.GetComponent<Enemy>()!=null)
{
Enemy enemy = collision.GetComponent<Enemy>();
enemy.Damage();
enemy.StartCoroutine("FreezeTimeFor",freezeTimeDuration);
}
SetupTargetsForBounce(collision);
StuckInto(collision);
}
在SkillManager中设置FreezeTimeDuration。
这样可能会遇到一点问题,骷髅被击中时会转身,冻结期间会抖动。
骷髅转身问题,只需要延长骷髅的地面检测距离就可以。因为骷髅被击退时会向上移动一点,地面检测过短会导致问题。

抖动是因为骷髅碰撞盒过大,不断与剑碰撞,修改到合适大小即可

修改后效果如下:
教程里给弹跳和盘旋类型也加了时间冻结,但我觉得这两个连续攻击的不加会流畅一些,这里就不添加了。而且我不想这两种类型第一次攻击时冻结敌人,于是自己改了一下:
csharp
{
if (isReturning)
return;
if(collision.GetComponent<Enemy>()!=null)
{
Enemy enemy = collision.GetComponent<Enemy>();
enemy.Damage();
if (!isBouncing && !isSpinning)
enemy.StartCoroutine("FreezeTimeFor", freezeTimeDuration);
}
SetupTargetsForBounce(collision);
StuckInto(collision);
}
总结 完整代码
Enemy.cs
添加敌人被冻结时的反馈。
csharp
//Enemy:敌人基类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : Entity
{
[SerializeField] protected LayerMask WhatIsPlayer;
[Header("Stunned Info")]
public float stunDuration = 1.0f;
public Vector2 stunDirection;
protected bool canBeStunned;
[SerializeField] protected GameObject counterImage;
[Header("Move Info")]
public float moveSpeed = 1.5f;
public float idleTime = 2.0f;
public float battleTime = 4.0f;
private float defaultMoveSpeed;
[Header("Attack Info")]
public float attackDistance;
public float attackCoolDown;
[HideInInspector] public float lastTimeAttacked;
public EnemyStateMachine stateMachine;
protected override void Awake()
{
base.Awake();
stateMachine = new EnemyStateMachine();
defaultMoveSpeed = moveSpeed;
}
protected override void Update()
{
base.Update();
stateMachine.currentState.Update();
}
public virtual void FreezeTime(bool _timeFrozen)
{
if(_timeFrozen)
{
moveSpeed = 0;
anim.speed = 0;
}
else
{
moveSpeed = defaultMoveSpeed;
anim.speed = 1;
}
}
protected virtual IEnumerator FreezeTimeFor(float _seconds)
{
FreezeTime(true);
yield return new WaitForSeconds(_seconds);
FreezeTime(false);
}
#region Counter Attack Window
//打开反击窗口
public virtual void OpenCounterAttackWindow()
{
canBeStunned = true;
counterImage.SetActive(true);
}
//关闭反击窗口
public virtual void CloseCounterAttackWindow()
{
canBeStunned = false;
counterImage.SetActive(false);
}
#endregion
//检查击晕条件,关闭反击窗口
public virtual bool CheckCanBeStunned()
{
if(canBeStunned)
{
CloseCounterAttackWindow();
return true;
}
return false;
}
//设置触发器
public virtual void AnimationTrigger() => stateMachine.currentState.AnimationFinishTrigger();
public virtual RaycastHit2D IsPlayerDetected()=>Physics2D.Raycast(transform.position, Vector2.right * facingDir, 50 ,WhatIsPlayer);
protected override void OnDrawGizmos()
{
base.OnDrawGizmos();
Gizmos.color = Color.yellow;
Gizmos.DrawLine(transform.position, new Vector3(transform.position.x + attackDistance * facingDir, transform.position.y));
}
}
Sword_Skill.cs
添加冻结时长变量,并传至控制器。
csharp
//Sword_Skill:掷剑技能
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum SwordType
{
Regular,
Bounce,
Pierce,
Spin
}
public class Sword_Skill : Skill
{
public SwordType swordType = SwordType.Regular;
[Header("Skill Info")]
[SerializeField] private GameObject swordPrefab;
[SerializeField] private Vector2 launchForce;
[SerializeField] private float swordGravity;
[SerializeField] private float returnSpeed;
[SerializeField] private float freezeTimeDuration;
[Header("Aim dots")]
[SerializeField] private int numberOfDots;
[SerializeField] private float spaceBetweenDots;
[SerializeField] private GameObject dotPrefab;
[SerializeField] private Transform dotsParent;
private GameObject[] dots;
private Vector2 finalDir;
[Header("Bounce Info")]
[SerializeField] private float bounceGravity;
[SerializeField] private int bounceAmount;
[SerializeField] private float bounceSpeed;
[Header("Pierce Info")]
[SerializeField] private int pierceAmount;
[SerializeField] private float pierceGravity;
[Header("Spin Info")]
[SerializeField] private float maxTravelDistance;
[SerializeField] private float spinDuration;
[SerializeField] private float spinGravity;
[SerializeField] private float hitCooldown;
protected override void Start()
{
base.Start();
GenerateDots();
SetupGravity();
}
protected override void Update()
{
if(Input.GetKeyUp(KeyCode.Mouse1))
finalDir = new Vector2(AimDirection().normalized.x * launchForce.x , AimDirection().normalized.y * launchForce.y);
if(Input.GetKey(KeyCode.Mouse1))
{
for(int i = 0; i < dots.Length; i++)
{
dots[i].transform.position = DotsPosition(i * spaceBetweenDots);
}
}
}
private void SetupGravity()
{
if (swordType == SwordType.Bounce)
swordGravity = bounceGravity;
else if (swordType == SwordType.Pierce)
swordGravity = pierceGravity;
else if(swordType == SwordType.Spin)
swordGravity = spinGravity;
}
public void CreateSword()
{
GameObject newSword = Instantiate(swordPrefab, player.transform.position, transform.rotation);
Sword_Skill_Controller newSwordScript = newSword.GetComponent<Sword_Skill_Controller>();
if (swordType == SwordType.Bounce)
newSwordScript.SetupBounce(true, bounceAmount, bounceSpeed);
else if (swordType == SwordType.Pierce)
newSwordScript.SetupPierce(pierceAmount);
else if (swordType== SwordType.Spin)
newSwordScript.SetupSpin(true, maxTravelDistance, spinDuration, hitCooldown);
newSwordScript.SetupSword(finalDir, swordGravity, player, returnSpeed,freezeTimeDuration);
player.AssignNewSword(newSword);
DotsActive(false);
}
#region 瞄准
public Vector2 AimDirection()
{
Vector2 playerPosition = player.transform.position;
Vector2 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
Vector2 direction = mousePosition - playerPosition;
return direction;
}
private void GenerateDots()
{
dots = new GameObject[numberOfDots];
for (int i = 0; i < numberOfDots; i++)
{
dots[i] = Instantiate(dotPrefab, player.transform.position, Quaternion.identity, dotsParent);
dots[i].SetActive(false);
}
}
public void DotsActive(bool _isActive)
{
for (int i = 0; i < dots.Length; i++)
{
dots[i].SetActive(_isActive);
}
}
private Vector2 DotsPosition(float t)
{
Vector2 position = (Vector2)player.transform.position +
new Vector2(AimDirection().normalized.x * launchForce.x, AimDirection().normalized.y * launchForce.y) * t +
0.5f * (Physics2D.gravity * swordGravity) * (t * t);
return position;
}
#endregion
}
Sword_Skill_Controller.cs
接收冻结时长参数。调用携程冻结敌人。
csharp
//Sword_Skill_Controller:掷剑技能控制器
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Sword_Skill_Controller : MonoBehaviour
{
private Animator anim;
private Rigidbody2D rb;
private CircleCollider2D cd;
private Player player;
private bool canRotate = true;
private bool isReturning;
private float returnSpeed = 12;
private float freezeTimeDuration;
[Header("Bounce Info")]
private bool isBouncing;
private int bounceAmount;
private float bounceSpeed;
private List<Transform> enemyTarget;
private int targetIndex;
[Header("Pierce Info")]
private float pierceAmount;
[Header("Spin Info")]
private bool isSpinning;
private float maxTravelDistance;
private float spinDuration;
private float spinTimer;
private bool wasStopped;
private float hitTimer;
private float hitCooldown;
private void Awake()
{
anim = GetComponentInChildren<Animator>();
rb = GetComponent<Rigidbody2D>();
cd = GetComponent<CircleCollider2D>();
}
private void DestroyMe()
{
Destroy(gameObject);
}
public void SetupSword(Vector2 _dir, float _gravityScale, Player _player, float _returnSpeed, float _freezeTimeDuration)
{
player = _player;
rb.velocity = _dir;
rb.gravityScale = _gravityScale;
returnSpeed = _returnSpeed;
freezeTimeDuration = _freezeTimeDuration;
if (pierceAmount<=0)
anim.SetBool("Rotation", true);
Invoke("DestroyMe", 7.0f);
}
public void SetupBounce(bool _isBouncing, int _bounceAmount , float _bounceSpeed)
{
isBouncing = _isBouncing;
bounceAmount = _bounceAmount;
bounceSpeed = _bounceSpeed;
enemyTarget = new List<Transform>();
}
public void SetupPierce(int _pierceAmount)
{
pierceAmount = _pierceAmount;
}
public void SetupSpin(bool _isSpinning, float _maxTravelDistance,float _spinDuration, float _hitCooldown)
{
isSpinning = _isSpinning;
maxTravelDistance = _maxTravelDistance;
spinDuration = _spinDuration;
hitCooldown = _hitCooldown;
}
public void ReturnSword()
{
rb.constraints = RigidbodyConstraints2D.FreezeAll;
transform .parent = null;
isReturning = true;
}
private void Update()
{
if (canRotate)
{
transform.right = rb.velocity;
}
if (isReturning)
{
transform.position = Vector2.MoveTowards(transform.position, player.transform.position, returnSpeed * Time.deltaTime);
if (Vector2.Distance(transform.position, player.transform.position) < 1)
player.CatchTheSword();
}
BounceLogic();
SpinLogic();
}
private void SpinLogic()
{
if (isSpinning)
{
if (Vector2.Distance(player.transform.position, transform.position) > maxTravelDistance && !wasStopped)
{
StopWhenSpinning();
}
}
if (wasStopped)
{
spinTimer -= Time.deltaTime;
if (spinTimer < 0)
{
isReturning = true;
isSpinning = false;
}
hitTimer -= Time.deltaTime;
if (hitTimer < 0)
{
hitTimer = hitCooldown;
Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, 1);
foreach (var hit in colliders)
{
if (hit.GetComponent<Enemy>() != null)
hit.GetComponent<Enemy>().Damage();
}
}
}
}
private void BounceLogic()
{
if (isBouncing && enemyTarget.Count > 0)
{
transform.position = Vector2.MoveTowards(transform.position, enemyTarget[targetIndex].position, bounceSpeed * Time.deltaTime);
if (Vector2.Distance(transform.position, enemyTarget[targetIndex].position) < 0.1f)
{
enemyTarget[targetIndex].GetComponent<Enemy>().Damage();
targetIndex = (targetIndex + 1) % enemyTarget.Count;
bounceAmount--;
if (bounceAmount <= 0)
{
isBouncing = false;
isReturning = true;
}
}
}
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (isReturning)
return;
if(collision.GetComponent<Enemy>()!=null)
{
Enemy enemy = collision.GetComponent<Enemy>();
enemy.Damage();
if (!isBouncing && !isSpinning)
enemy.StartCoroutine("FreezeTimeFor", freezeTimeDuration);
}
SetupTargetsForBounce(collision);
StuckInto(collision);
}
private void SetupTargetsForBounce(Collider2D collision)
{
if (collision.GetComponent<Enemy>() != null)
{
if (isBouncing && enemyTarget.Count <= 0)
{
Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, 10);
foreach (var hit in colliders)
{
if (hit.GetComponent<Enemy>() != null)
enemyTarget.Add(hit.transform);
}
}
}
}
private void StopWhenSpinning()
{
wasStopped = true;
rb.constraints = RigidbodyConstraints2D.FreezePosition;
spinTimer = spinDuration;
}
private void StuckInto(Collider2D collision)
{
if(pierceAmount >0 && collision.GetComponent<Enemy>() != null)
{
pierceAmount--;
return;
}
if (isSpinning)
{
//StopWhenSpinning();
return;
}
canRotate = false;
cd.enabled = false;
rb.isKinematic = true;
rb.constraints = RigidbodyConstraints2D.FreezeAll;
if(isBouncing && enemyTarget.Count > 0)
return;
anim.SetBool("Rotation", false);
transform.parent = collision.transform;
}
}