Unity开发2D类银河恶魔城游戏学习笔记
Unity教程(零)Unity和VS的使用相关内容
Unity教程(一)开始学习状态机
Unity教程(二)角色移动的实现
Unity教程(三)角色跳跃的实现
Unity教程(四)碰撞检测
Unity教程(五)角色冲刺的实现
Unity教程(六)角色滑墙的实现
Unity教程(七)角色蹬墙跳的实现
Unity教程(八)角色攻击的基本实现
Unity教程(九)角色攻击的改进
Unity教程(十)Tile Palette搭建平台关卡
Unity教程(十一)相机
Unity教程(十二)视差背景
Unity教程(十三)敌人状态机
Unity教程(十四)敌人空闲和移动的实现
Unity教程(十五)敌人战斗状态的实现
Unity教程(十六)敌人攻击状态的实现
Unity教程(十七)敌人战斗状态的完善
Unity教程(十八)战斗系统 攻击逻辑
Unity教程(十九)战斗系统 受击反馈
Unity教程(二十)战斗系统 角色反击
如果你更习惯用知乎
Unity开发2D类银河恶魔城游戏学习笔记目录
文章目录
前言
本文为Udemy课程The Ultimate Guide to Creating an RPG Game in Unity学习笔记,如有错误,欢迎指正。
本节实现角色技能系统基础部分。
对应视频:
Concept of a Skill System
Creating Player Manager and Skill Manager
Foundation of Skill System
一、概述
本节开始进入技能系统的实现。
先使用单例模式创建玩家管理器PlayerManager和技能管理器SkillManager用于全局访问。
创建一个技能基类,所有技能都将在此基础上创建。
修改冲刺,将它重写为一个技能。
二、单例模式
在前面的章节中,许多类中都需要用到player。例如SkeletonBattle中,使用GameObject.Find会在所有对象中遍历查找,效率很低。
单例模式就可以用来解决这一点,它用来解决频繁创建和销毁全局使用的类实例的问题。
单例模式确保一个类只有一个实例,并提供一个全局访问点来访问该实例。
Unity中单例模式实现方式多种多样,在此处我们使用继承于MonoBehaviour的写法,因为我们需要PlayerManager与Unity的组件系统交互。
单例的写法总结,可以参考以下文章:
Unity单例模式设计和应用
Unity单例模式写法总结
Unity单例有几种写法
unity中最基础的是
public class PlayerManager : MonoBehaviour
{
public static PlayerManager instance;
private void Awake()
{
instance = this;
}
}
在教程中,为了确保只有一个单例,添加了一些处理:
public class PlayerManager : MonoBehaviour
{
public static PlayerManager instance;
private void Awake()
{
if(instance != null)
Destroy(instance.gameObject);
else
instance = this;
}
}
这样写是为了让运行后多余的PlayerManager被销毁只保留一个。但尝试之后发现由于Awake执行顺序,这导致最后可能无法完全销毁多余的物体。
我做了一些修改,由每次销毁原来的实例改为每次销毁新创建的,这样最后会正常地只剩一个PlayerManager
public class PlayerManager : MonoBehaviour
{
public static PlayerManager instance;
public Player player;
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(this.gameObject);
}
else
{
instance = this;
}
}
}
三、玩家管理器PlayerManager
在开始之前我们先整理一下相机。
创建一个空项目命名为Camera,把Main Camera和Virtual Camera都挂在下面。
把实体特效脚本EntityFX拖到Scripts文件夹下
正式开始PlayerManager的创建。
使用单例模式创建PlayerManager,在其中添加变量player,这样就可以在全局以类名.instance.成员名的形式访问它了。
//PlayerManager:玩家管理器
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerManager : MonoBehaviour
{
public static PlayerManager instance;
public Player player;
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(this.gameObject);
}
else
{
instance = this;
}
}
}
替换其他脚本中查询获取player的部分。Ctrl+F搜索vs项目找到需要替换的地方。
SkeletonBattleState和SkeletonGroundedState中
public override void Enter()
{
base.Enter();
player = PlayerManager.instance.player.transform;
}
创建空对象并命名为PlayerManager,将脚本PlayerManager挂载到下面。将目前的玩家对象Player赋予变量。
在这里我们测试一下,把PlayerManager复制几份并运行。
多余的PlayerManager会销毁,只剩一个。
三、技能管理器SkillManager
同PlayerManager类似,创建一个SkillManager脚本,再创建一个空物体命名为SkillManager,将脚本挂上去。
SkillManager同样使用单例模式,便于全局访问。
//SkillManager:玩家管理器
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SkillManager : MonoBehaviour
{
public static SkillManager instance;
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(this.gameObject);
}
else
{
instance = this;
}
}
}
四、Skill基类
我们创建一个Skill基类,所有技能都继承于它。
技能大都有冷却时间,我们把它写到Skill基类里。在基类中添加冷却时间cooldown,然后添加冷却时间计时器cooldownTimer。在Update函数中更新计时器。
[SerializeField] protected float cooldown;
protected float cooldownTimer;
protected virtual void Update()
{
cooldownTimer -= Time.deltaTime;
}
我们还需要创建一个CanUseSkill函数判断冷却时间是否结束,技能是否可用。
还需有一个实现技能具体功能的函数UseSkill,在技能可用时,调用UseSkill函数释放技能。
public virtual bool CanUseSkill()
{
if(cooldownTimer< 0)
{
UseSkill();
cooldownTimer = cooldown;
return true;
}
Debug.Log("Skill is on cooldown");
return false;
}
public virtual void UseSkill()
{
//技能实现
}
这里教程中的逻辑是,只要判断了技能是否可用,在可用时就立即释放并重置计时器。个人认为这里两部分分开写会比较好,但前面章节都是这个逻辑写的,就先按教程中的写吧。
五、创建Dash_Skill冲刺技能
前面的章节中我们已经实现了冲刺状态,现在我们可以把它改成技能。
创建Dash_Skill脚本,它继承自Skill基类,重写UseSkill函数。
//Dash_Skill:冲刺技能
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Dash_Skill : Skill
{
public override void UseSkill()
{
base.UseSkill();
}
}
把Dash_Skill脚本挂到SkillManager下
在SkillManager脚本中创建冲刺技能并赋值。
public Dash_Skill dash { get; private set; }
private void Start()
{
dash = GetComponent<Dash_Skill>();
}
现在可以把冲刺状态实现时的冲刺冷却和计时器删掉修改成调用Dash_Skill了。
在Player的CheckForInput中
[Header("Dash Info")]
public float dashSpeed=25f;
public float dashDuration=0.2f;
public float dashDir { get; private set; }
//检查冲刺输入
public void CheckForDashInput()
{
if (Input.GetKeyDown(KeyCode.LeftShift) && SkillManager.instance.dash.CanUseSkill())
{
dashDir = Input.GetAxisRaw("Horizontal");
if (dashDir == 0)
dashDir = facingDir;
StateMachine.ChangeState(dashState);
}
}
给冷却时间重新赋值
运行查看冲刺是否正常:
总结 完整代码
PlayerManager.cs
玩家管理器
//PlayerManager:玩家管理器
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerManager : MonoBehaviour
{
public static PlayerManager instance;
public Player player;
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(this.gameObject);
}
else
{
instance = this;
}
}
}
SkillManager.cs
技能管理器,创建冲刺技能
//SkillManager:玩家管理器
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SkillManager : MonoBehaviour
{
public static SkillManager instance;
public Dash_Skill dash { get; private set; }
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(this.gameObject);
}
else
{
instance = this;
}
}
private void Start()
{
dash = GetComponent<Dash_Skill>();
}
}
Skill.cs
技能基类,包含冷却时间和计时器,包含技能是否可用的判定和技能实现
//Skill:技能基类
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class Skill : MonoBehaviour
{
[SerializeField] protected float cooldown;
protected float cooldownTimer;
protected virtual void Update()
{
cooldownTimer -= Time.deltaTime;
}
public virtual bool CanUseSkill()
{
if(cooldownTimer< 0)
{
UseSkill();
cooldownTimer = cooldown;
return true;
}
Debug.Log("Skill is on cooldown");
return false;
}
public virtual void UseSkill()
{
//技能实现
}
}
Dash_Skill.cs
冲刺技能
//Dash_Skill:冲刺技能
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Dash_Skill : Skill
{
public override void UseSkill()
{
base.UseSkill();
}
}
Player.cs
删除冲刺冷却和计时器,修改冲刺条件
//Player:玩家
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : Entity
{
[Header("Attack details")]
public Vector2[] attackMovement;
public float counterAttackDuration = 0.2f;
public bool isBusy { get; private set; }
[Header("Move Info")]
public float moveSpeed = 8f;
public float jumpForce = 12f;
[Header("Dash Info")]
public float dashSpeed=25f;
public float dashDuration=0.2f;
public float dashDir { get; private set; }
#region 状态
public PlayerStateMachine StateMachine { get; private set; }
public PlayerIdleState idleState { get; private set; }
public PlayerMoveState moveState { get; private set; }
public PlayerJumpState jumpState { get; private set; }
public PlayerAirState airState { get; private set; }
public PlayerDashState dashState { get; private set; }
public PlayerWallSlideState wallSlideState { get; private set; }
public PlayerWallJumpState wallJumpState { get; private set; }
public PlayerPrimaryAttackState primaryAttack { get; private set; }
public PlayerCounterAttackState counterAttack { get; private set; }
#endregion
//创建对象
protected override void Awake()
{
base.Awake();
StateMachine = new PlayerStateMachine();
idleState = new PlayerIdleState(StateMachine, this, "Idle");
moveState = new PlayerMoveState(StateMachine, this, "Move");
jumpState = new PlayerJumpState(StateMachine, this, "Jump");
airState = new PlayerAirState(StateMachine, this, "Jump");
dashState = new PlayerDashState(StateMachine, this, "Dash");
wallSlideState = new PlayerWallSlideState(StateMachine, this, "WallSlide");
wallJumpState = new PlayerWallJumpState(StateMachine, this, "Jump");
primaryAttack = new PlayerPrimaryAttackState(StateMachine, this, "Attack");
counterAttack = new PlayerCounterAttackState(StateMachine, this, "CounterAttack");
}
// 设置初始状态
protected override void Start()
{
base.Start();
StateMachine.Initialize(idleState);
}
// 更新
protected override void Update()
{
base.Update();
StateMachine.currentState.Update();
CheckForDashInput();
}
public IEnumerator BusyFor(float _seconds)
{
isBusy = true;
yield return new WaitForSeconds(_seconds);
isBusy = false;
}
//设置触发器
public void AnimationTrigger() => StateMachine.currentState.AnimationFinishTrigger();
//检查冲刺输入
public void CheckForDashInput()
{
if (Input.GetKeyDown(KeyCode.LeftShift) && SkillManager.instance.dash.CanUseSkill())
{
dashDir = Input.GetAxisRaw("Horizontal");
if (dashDir == 0)
dashDir = facingDir;
StateMachine.ChangeState(dashState);
}
}
}
SkeletonBattleState.cs
修改查找Player的部分
//SkeletonBattleState:骷髅战斗状态
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SkeletonBattleState : EnemyState
{
private Transform player;
private Enemy_Skeleton enemy;
private int moveDir;
public SkeletonBattleState(EnemyStateMachine _stateMachine, Enemy _enemyBase, Enemy_Skeleton _enemy, string _animBoolName) : base(_stateMachine, _enemyBase, _animBoolName)
{
this.enemy =_enemy;
}
public override void Enter()
{
base.Enter();
player = PlayerManager.instance.player.transform;
}
public override void Exit()
{
base.Exit();
}
public override void Update()
{
base.Update();
if (enemy.IsPlayerDetected())
{
stateTimer = enemy.battleTime;
if (enemy.IsPlayerDetected().distance < enemy.attackDistance)
{
if(CanAttack())
stateMachine.ChangeState(enemy.attackState);
}
}
else
{
if(stateTimer <0 || Vector2.Distance(enemy.transform.position,player.transform.position)>10)
stateMachine.ChangeState(enemy.idleState);
}
if(player.position.x > enemy.transform.position.x)
moveDir = 1;
else if(player.position.x < enemy.transform.position.x)
moveDir = -1;
enemy.SetVelocity(enemy.moveSpeed * moveDir, rb.velocity.y);
}
private bool CanAttack()
{
if (Time.time >= enemy.lastTimeAttacked + enemy.attackCoolDown)
return true;
return false;
}
}
SkeletonGroundedState.cs
修改查找Player的部分
//SkeletonGroundedState:骷髅接地状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SkeletonGroundedState : EnemyState
{
protected Enemy_Skeleton enemy;
protected Transform player;
public SkeletonGroundedState(EnemyStateMachine _stateMachine, Enemy _enemyBase, Enemy_Skeleton _enemy, string _animBoolName) : base(_stateMachine, _enemyBase, _animBoolName)
{
this.enemy=_enemy;
}
public override void Enter()
{
base.Enter();
//获取Player的Transform组件
player = PlayerManager.instance.player.transform;
}
public override void Exit()
{
base.Exit();
}
public override void Update()
{
base.Update();
//转换到战斗状态
if (enemy.IsPlayerDetected() || Vector2.Distance(enemy.transform.position,player.position) < 2)
stateMachine.ChangeState(enemy.battleState);
}
}