Unity开发2D类银河恶魔城游戏学习笔记
Unity教程(零)Unity和VS的使用相关内容
Unity教程(一)开始学习状态机
Unity教程(二)角色移动的实现
Unity教程(三)角色跳跃的实现
Unity教程(四)碰撞检测
Unity教程(五)角色冲刺的实现
Unity教程(六)角色滑墙的实现
Unity教程(七)角色蹬墙跳的实现
Unity教程(八)角色攻击的基本实现
Unity教程(九)角色攻击的改进
Unity教程(十)Tile Palette搭建平台关卡
Unity教程(十一)相机
Unity教程(十二)视差背景
Unity教程(十三)敌人状态机
Unity教程(十四)敌人空闲和移动的实现
Unity教程(十五)敌人战斗状态的实现
如果你更习惯用知乎
Unity开发2D类银河恶魔城游戏学习笔记目录
文章目录
- Unity开发2D类银河恶魔城游戏学习笔记
- 前言
- 一、概述
- 二、实现Player的检测
- 三、创建接地状态
- 三、实现战斗状态
- (1)创建战斗状态
- (2)状态切换
- 总结 完整代码
- Enemy.cs
- SkeletonGroundedState.cs
- SkeletonIdleState.cs
- SkeletonMoveState.cs
- SkeletonBattleState.cs
- Enemy_Skeleton.cs
前言
本文为Udemy课程The Ultimate Guide to Creating an RPG Game in Unity学习笔记,如有错误,欢迎指正。
本节实现敌人的战斗状态。
对应b站视频:
【Unity教程】从0编程制作类银河恶魔城游戏P50
一、概述
本节中我们实现骷髅的战斗状态。
骷髅小怪在检测到玩家时,会向着玩家移动。在移动到一定距离时停止移动进入攻击状态。
由于我们想让骷髅在空闲和移动时均能转入战斗状态,我们创建超级状态接地状态,直接写接地状态到战斗状态的切换即可。
状态切换条件如下:
二、实现Player的检测
玩家的检测在Enemy基类中实现。
碰撞检测的详细讲解见Unity教程(四)碰撞检测
这里我们不再仅仅使用bool类型作为函数的返回值,而是使用RaycastHit2D返回更详细的信息。
RaycastHit2DUnity官方手册
Raycast包含的变量如下表:
变量 | 介绍 |
---|---|
centroid | 用于执行投射的图元的质心 |
collider | 射线命中的碰撞体 |
distance | 从射线原点到撞击点的距离 |
fraction | 射线上发生命中的距离的分数 |
normal | 射线命中的表面的法线矢量 |
point | 世界空间中射线命中碰撞体表面的点 |
rigidbody | 附加到命中的对象的 Rigidbody2D |
transform | 命中的对象的变换 |
我们要新建一个过滤器WhatIsPlayer进行检测。
这里我们以骷髅的位置为起点,向它面向的方向发射射线检测,检测的最大距离设置为50.
为了方便调试我们还要绘制出攻击检测的线条,在Enemy里对实体中的OnDrawGizmos()函数进行重写。
攻击检测的线条绘制:起点为Enemy_Skeleton的位置,向右attackDistance找到终点。
在Enemy中添加如下代码:
[SerializeField] protected LayerMask WhatIsPlayer;
[Header("Attack Info")]
public float attackDistance;
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));
}
将Enemy_Skeleton的大小调大一点
创建一个新的WallCheck,并拖到Enemy_Skeleton中
将Wallcheck往下拖一些,以免与攻击检测重合。
在Enemy中重写OnDrawGizmos(),换一个颜色绘制出攻击检测。
给attackDistancce赋一个恰当的值。
将Enemy_Skeleton中WhatIsPlayer改为Player
新建Player和Enemy层。
Player改为Player层,Enemy_Skeleton改为Enemy层,修改时选择将它们所有子物体也改为对应层次。
三、创建接地状态
创建接地状态SkeletonGroundedState,它继承自EnemyState。
在子菜单中创建构造函数,生成重写。
添加Enemy_Skeleton enermy,传递骷髅小怪独有的变量和函数,并修改构造函数。
//SkeletonGroundedState:骷髅接地状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SkeletonGroundedState : EnemyState
{
protected Enemy_Skeleton enemy;
public SkeletonGroundedState(EnemyStateMachine _stateMachine, Enemy _enemyBase, Enemy_Skeleton _enemy,string _animBoolName) : base(_stateMachine, _enemyBase, _animBoolName)
{
enemy=_enemy;
}
public override void Enter()
{
base.Enter();
}
public override void Exit()
{
base.Exit();
}
public override void Update()
{
base.Update();
}
}
将SkeletonIdleState和SkeletonMoveState的父类改为SkeletonGroundedState,这时两个子类构造函数参数与父类相同,可以删除原有构造函数和Enemy_Skeleton enemy,直接在子菜单重新生成一个构造函数了。
三、实现战斗状态
(1)创建战斗状态
创建战斗状态SkeletonBattleState,它继承自EnemyState。
在子菜单中创建构造函数,生成重写。添加Enemy_Skeleton enermy,并修改构造函数。
private Enemy_Skeleton enemy;
public SkeletonBattleState(EnemyStateMachine _stateMachine, Enemy _enemyBase, Enemy_Skeleton _enemy, string _animBoolName) : base(_stateMachine, _enemyBase, _animBoolName)
{
enemy=_enemy;
}
在EnemySkeleton类中创建battleState。这里条件变量仍然选择“Move”,因为战斗状态仍然播放移动动画。
#region 状态
public SkeletonIdleState idleState { get; private set; }
public SkeletonMoveState moveState { get; private set; }
public SkeletonBattleState battleState { get; private set; }
#endregion
protected override void Awake()
{
base.Awake();
idleState = new SkeletonIdleState(stateMachine,this,this,"Idle");
moveState = new SkeletonMoveState(stateMachine, this,this, "Move");
battleState = new SkeletonBattleState(stateMachine, this, this, "Move");
}
(2)状态切换
当检测到玩家时,骷髅由接地状态转换为战斗状态。
在SkeletonGroundedState的Update()函数中添加
public override void Update()
{
base.Update();
if(enemy.IsPlayerDetected())
stateMachine.ChangeState(enemy.battleState);
}
骷髅进入战斗状态时,要随玩家位置的变化改变移动方向。因此我们引入player的位置这个变量,通过它与骷髅位置的对比确定战斗状态骷髅移动的方向。
player.position.x > enemy.transform.position.x,玩家在骷髅右边,移动方向向右
player.position.x < enemy.transform.position.x,玩家在骷髅左边,移动方向向左
//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)
{
enemy=_enemy;
}
public override void Enter()
{
base.Enter();
player = GameObject.Find("Player").transform;
}
public override void Exit()
{
base.Exit();
}
public override void Update()
{
base.Update();
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, enemy.rb.velocity.y);
}
}
效果如下:
而当玩家进入骷髅的攻击距离时,骷髅停止移动切换为攻击状态。根据上述内容所讲,骷髅与玩家的距离可由RaycastHit2D里的distance获得。攻击状态我们先控制输出代替,具体攻击状态内容我们下一节再进行实现。
Update中添加如下代码:
public override void Update()
{
base.Update();
if (enemy.IsPlayerDetected())
{
if (enemy.IsPlayerDetected().distance < enemy.attackDistance)
{
Debug.Log("attack");
enemy.ZeroVelocity();
return;
}
}
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, enemy.rb.velocity.y);
}
总结 完整代码
Enemy.cs
添加碰撞检测和攻击距离变量,实现攻击检测的函数和攻击检测的绘制
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : Entity
{
[SerializeField] protected LayerMask WhatIsPlayer;
[Header("Move Info")]
public float moveSpeed = 1.5f;
public float idleTime = 2.0f;
[Header("Attack Info")]
public float attackDistance;
public EnemyStateMachine stateMachine;
protected override void Awake()
{
base.Awake();
stateMachine = new EnemyStateMachine();
}
protected override void Update()
{
base.Update();
stateMachine.currentState.Update();
}
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));
}
}
SkeletonGroundedState.cs
创建接地状态,由移动和空闲状态继承。实现切换到战斗状态。
//SkeletonGroundedState:骷髅接地状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SkeletonGroundedState : EnemyState
{
protected Enemy_Skeleton enemy;
public SkeletonGroundedState(EnemyStateMachine _stateMachine, Enemy _enemyBase, Enemy_Skeleton _enemy,string _animBoolName) : base(_stateMachine, _enemyBase, _animBoolName)
{
enemy=_enemy;
}
public override void Enter()
{
base.Enter();
}
public override void Exit()
{
base.Exit();
}
public override void Update()
{
base.Update();
if (enemy.IsPlayerDetected())
stateMachine.ChangeState(enemy.battleState);
}
}
SkeletonIdleState.cs
修改构造函数
//SkeletonIdleState:骷髅空闲状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SkeletonIdleState : SkeletonGroundedState
{
public SkeletonIdleState(EnemyStateMachine _stateMachine, Enemy _enemyBase, Enemy_Skeleton enemy, string _animBoolName) : base(_stateMachine, _enemyBase, enemy, _animBoolName)
{
}
public override void Enter()
{
base.Enter();
stateTimer = enemy.idleTime;
}
public override void Exit()
{
base.Exit();
}
public override void Update()
{
base.Update();
if (stateTimer < 0)
stateMachine.ChangeState(enemy.moveState);
}
}
SkeletonMoveState.cs
//SkeletonMoveState:骷髅移动状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SkeletonMoveState : SkeletonGroundedState
{
public SkeletonMoveState(EnemyStateMachine _stateMachine, Enemy _enemyBase, Enemy_Skeleton enemy, string _animBoolName) : base(_stateMachine, _enemyBase, enemy, _animBoolName)
{
}
public override void Enter()
{
base.Enter();
}
public override void Exit()
{
base.Exit();
}
public override void Update()
{
base.Update();
enemy.SetVelocity(enemy.moveSpeed*enemy.facingDir,enemy.rb.velocity.y);
if(!enemy.isGroundDetected() || enemy.isWallDetected())
{
enemy.Flip();
stateMachine.ChangeState(enemy.idleState);
}
}
}
SkeletonBattleState.cs
创建战斗状态,实现切换到攻击状态,和随玩家移动改变方向。
//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)
{
enemy=_enemy;
}
public override void Enter()
{
base.Enter();
player = GameObject.Find("Player").transform;
}
public override void Exit()
{
base.Exit();
}
public override void Update()
{
base.Update();
if (enemy.IsPlayerDetected())
{
if (enemy.IsPlayerDetected().distance < enemy.attackDistance)
{
Debug.Log("attack");
enemy.ZeroVelocity();
return;
}
}
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, enemy.rb.velocity.y);
}
}
Enemy_Skeleton.cs
创建战斗状态
//Enemy_Skeleton:骷髅敌人
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy_Skeleton : Enemy
{
#region 状态
public SkeletonIdleState idleState { get; private set; }
public SkeletonMoveState moveState { get; private set; }
public SkeletonBattleState battleState { get; private set; }
#endregion
protected override void Awake()
{
base.Awake();
idleState = new SkeletonIdleState(stateMachine,this,this,"Idle");
moveState = new SkeletonMoveState(stateMachine, this,this, "Move");
battleState = new SkeletonBattleState(stateMachine, this, this, "Move");
}
protected override void Start()
{
base.Start();
stateMachine.Initialize(idleState);
}
protected override void Update()
{
base.Update();
}
}