# 观前提醒
项目会在 GitHub 中开源,链接:https://github.com/Maikire/UnityGameDemo/tree/main/01-02
本次制作的小游戏很适合初学者。
如果有什么问题或想法,欢迎各位在评论区留言。
# 小游戏
如上面所展示的,这是一种很常见很简单的小游戏,接下来让我们一步一步地来实现它。
# 需求分析
# 角色
- 上下左右移动,不能移出屏幕
- 自动攻击
- 有三个技能
- 角色没有生命值
# 子弹
- 一发子弹就能解决敌人
- 命中时生成爆炸的动画
# 技能
- 加快攻击速度,有冷却时间和持续时间,按键:Z
- 减慢已经生成的敌人的移动速度,有冷却时间和持续时间,按键:X
- 炸弹:延时爆炸,可以清屏,有使用次数,没有冷却时间,按键:C
# 城堡
- 有生命值,生命值为 0 时结束游戏
- 受击特效(变红色 + 生成一个爆炸的动画)
# 敌人
- 有两种,随机生成
- 移动速度快的攻击力低
- 移动速度慢的攻击力高
- 移动速度随游戏时间增加,有上限
- 生成速度随游戏时间增加,有上限
- 碰到城堡,减少城堡的生命值
# UI
- 分数、技能
- 结束游戏显示 UI
- 重新开始的按钮
# 其他
- 弹药箱:敌人移动速度达到最快时,随机生成,增加炸弹使用次数
- 分数:随游戏时间增加
- 按 V 键 重置游戏
# 池子
游戏中有几个物体会反复生成和清除,对于这样的情况,应该写一个池子(就是队列)
池子应该分为两种:敌人池,子弹池
# 敌人池
- 有两种敌人,必须要两个池子。如果只用一个池子,就会失去随机性(杀敌的顺序就是生成敌人的顺序,这显然不是我们想要的)。
- 再看生成随机的方法,只有 int 类型和 float 类型
- 所以,ID 就显得尤为重要
- 游戏内容较少,所以可以使用两个字典集合,一个存储游戏物体(一个 ID 对应一个物体),另一个存储队列(一个 ID 对应一个队列)
代码如下:
//ID: | |
//Alien Slug:0 | |
//Zombie:1 | |
public static EnemyPoolMnager Intance; // 使用这个类的入口 | |
public GameObject[] AllEnemyPrefabs; | |
private Dictionary<int, GameObject> AllEnemy; | |
private Dictionary<int, Queue<GameObject>> AllEnemyPool; | |
private void Awake() | |
{ | |
Intance = this; | |
} | |
private void Start() | |
{ | |
AllEnemy = new Dictionary<int, GameObject>(); | |
AllEnemyPool = new Dictionary<int, Queue<GameObject>>(); | |
for (int i = 0; i < AllEnemyPrefabs.Length; i++) | |
{ | |
AllEnemy.Add(i, AllEnemyPrefabs[i]); | |
AllEnemyPool.Add(i, new Queue<GameObject>()); | |
} | |
} | |
/// <summary> | |
/// 生成敌人 | |
/// </summary> | |
public GameObject GetEnemy(int id, Vector2 posion) | |
{ | |
Queue<GameObject> tempQueue; | |
GameObject tempEnemy; | |
AllEnemy.TryGetValue(id, out tempEnemy); | |
AllEnemyPool.TryGetValue(id, out tempQueue); | |
if (tempQueue.Count > 0) | |
{ | |
GameObject enemy = tempQueue.Dequeue(); | |
// 初始化数值 | |
enemy.transform.position = posion; | |
enemy.transform.rotation = Quaternion.identity; | |
enemy.GetComponent<EnemyController>().Speed = tempEnemy.GetComponent<EnemyController>().Speed; | |
enemy.SetActive(true); | |
return enemy; | |
} | |
else | |
{ | |
return Instantiate(tempEnemy, posion, Quaternion.identity); | |
} | |
} | |
/// <summary> | |
/// 回收敌人 | |
/// </summary> | |
public void RecoveryEnemy(int id, GameObject enemy) | |
{ | |
Queue<GameObject> tempQueue; | |
AllEnemyPool.TryGetValue(id, out tempQueue); | |
tempQueue.Enqueue(enemy); | |
enemy.SetActive(false); | |
} |
# 子弹池
包含子弹、爆炸效果,炸弹,炸弹箱。与敌人池的原理相同,这里不展示了
# 角色控制
组件如下:
关于动画:需要用到 Animator ,这里没有展示, 可以去看看项目
# 移动控制:使用 Rigidbody2D 中的 MovePosition
注:如果使用 transform.Translate ,当角色碰到碰撞器时继续向碰撞器移动,就会发生抖动,这不是我们想要的
代码如下:
[Tooltip("移动速度")] | |
public float Speed = 8; | |
private float Horizontal; // 左右 | |
private float Vertical; // 上下 | |
private Vector2 TargetPosion; // 目标位置 | |
private Rigidbody2D Player; | |
private void Start() | |
{ | |
TargetPosion = new Vector2(0, 0); | |
Player = this.GetComponent<Rigidbody2D>(); | |
} | |
private void Update() | |
{ | |
Move(); | |
} | |
/// <summary> | |
/// 控制移动 | |
/// </summary> | |
private void Move() | |
{ | |
Horizontal = Input.GetAxis("Horizontal"); | |
Vertical = Input.GetAxis("Vertical"); | |
if (Horizontal == 0 && Vertical == 0) | |
{ | |
return; | |
} | |
TargetPosion.x = Horizontal; | |
TargetPosion.y = Vertical; | |
Player.MovePosition(Player.position + TargetPosion * Speed * Time.deltaTime); | |
} |
# 自动攻击:自定义攻击速度
代码如下:
[Tooltip("攻击速度")] | |
public float FireSpeed = 1; | |
private float Timer = 0; | |
[HideInInspector] | |
public bool IsFire; // 是否开启射击 | |
private void Start() | |
{ | |
IsFire = true; | |
} | |
private void Update() | |
{ | |
if (IsFire) | |
{ | |
Timer += Time.deltaTime; | |
if (Timer > 1 / FireSpeed) | |
{ | |
BulletPoolMnager.Intance.GetBullet(0, this.transform.GetChild(0).position); | |
Timer = 0; | |
} | |
} | |
} |
# 城堡
应包含:生命值,判断游戏是否结束,受击特效(在下文中会讲到),改变血条,回收敌人
使用触发器
代码如下:
[Tooltip("生命值")] | |
public float Blood = 10; | |
[HideInInspector] | |
public float CurrentBlood; // 当前生命值 | |
private float PreviousBlood; // 上一帧的生命值 | |
private Transform BloodUI; // 血条 | |
private Vector3 ChangeScale; // 血条 | |
private Vector3 OriginalScale; // 初始的血条 | |
private SpriteRenderer ChangeColor; // 受击特效(改变颜色) | |
private float Timer; // 改变颜色 计时器 | |
[HideInInspector] | |
public bool IsGameOver = false; //true: GameOver | |
private void Start() | |
{ | |
CurrentBlood = Blood; | |
PreviousBlood = Blood; | |
BloodUI = this.transform.GetChild(0); | |
ChangeScale = BloodUI.localScale; | |
OriginalScale = BloodUI.localScale; | |
ChangeColor = this.GetComponent<SpriteRenderer>(); | |
} | |
private void Update() | |
{ | |
HitEffects(); | |
ChangeBloodUI(); | |
} | |
private void OnTriggerEnter2D(Collider2D collision) | |
{ | |
CurrentBlood -= collision.GetComponent<EnemyController>().Damage; | |
if (CurrentBlood <= 0) | |
{ | |
CurrentBlood = 0; | |
IsGameOver = true; | |
} | |
// 受击特效 | |
BulletPoolMnager.Intance.GetBullet(1, collision.transform.position); | |
ChangeColor.color = Color.red; | |
Timer = 0; | |
// 回收敌人 | |
EnemyPoolMnager.Intance.RecoveryEnemy(collision.GetComponent<EnemyController>().ID, collision.gameObject); | |
} | |
/// <summary> | |
/// 改变血条 | |
/// </summary> | |
private void ChangeBloodUI() | |
{ | |
if (PreviousBlood == CurrentBlood) | |
{ | |
return; | |
} | |
ChangeScale.x = OriginalScale.x * (CurrentBlood / Blood); | |
BloodUI.localScale = ChangeScale; | |
PreviousBlood = CurrentBlood; | |
} | |
/// <summary> | |
/// 清除受击特效 | |
/// </summary> | |
private void HitEffects() | |
{ | |
Timer += Time.deltaTime; | |
if (Timer > 0.1f) | |
{ | |
ChangeColor.color = Color.white; | |
} | |
} |
# 子弹、炸弹、炸弹箱、爆炸特效
这几个放在一起讲
# 子弹
使用触发器,触发了就生成特效、修改数值。移出屏幕了就回收。
代码如下:
[Tooltip("ID")] | |
public int ID; | |
[Tooltip("移动速度")] | |
public float Speed = 1; | |
private void Update() | |
{ | |
Move(); | |
if (Camera.main.WorldToScreenPoint(this.transform.position).x < -150) | |
{ | |
BulletPoolMnager.Intance.RecoveryBullet(ID, this.gameObject); | |
} | |
} | |
/// <summary> | |
/// 控制移动 | |
/// </summary> | |
private void Move() | |
{ | |
this.transform.Translate(-Speed * Time.deltaTime, 0, 0); | |
} | |
private void OnTriggerEnter2D(Collider2D collision) | |
{ | |
if (collision.tag != "BombBox") | |
{ | |
BulletPoolMnager.Intance.GetBullet(1, collision.transform.position); | |
EnemyPoolMnager.Intance.RecoveryEnemy(collision.GetComponent<EnemyController>().ID, collision.gameObject); | |
BulletPoolMnager.Intance.RecoveryBullet(ID, this.gameObject); | |
} | |
} |
# 炸弹
代码很简单:
[Tooltip("ID")] | |
public int ID; | |
[Tooltip("爆炸计时")] | |
public float BombTime = 1; | |
[HideInInspector] | |
public float BombTimer = 0; // 爆炸计时器 | |
private void Update() | |
{ | |
BombTimer += Time.deltaTime; | |
if (BombTimer > BombTime) | |
{ | |
GameObject temp = BulletPoolMnager.Intance.GetBullet(1, this.transform.position); | |
temp.transform.localScale = Vector3.one * 2; // 放大爆炸特效 | |
foreach (var item in GameObject.FindGameObjectsWithTag("Enemy")) | |
{ | |
EnemyPoolMnager.Intance.RecoveryEnemy(item.GetComponent<EnemyController>().ID, item); | |
} | |
BombTimer = 0; | |
BulletPoolMnager.Intance.RecoveryBullet(ID, this.gameObject); | |
} | |
} |
# 炸弹箱
使用触发器,撞到玩家增加炸弹使用次数即可,代码就不展示了
# 爆炸特效
这个比较特别,它是有动画的,而且只能播放一次,所以可以使用 Behaviour,如图:
代码很简单:
public class BulletExplosionBehaviour : StateMachineBehaviour | |
{ | |
// OnStateEnter is called when a transition starts and the state machine starts to evaluate this state | |
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) | |
{ | |
BulletPoolMnager.Intance.RecoveryBullet(1, animator.gameObject); | |
} | |
} |
# 敌人控制
代码很简单:
[Tooltip("ID")] | |
public int ID; | |
[Tooltip("移动速度")] | |
public float Speed = 1; | |
[Tooltip("伤害值")] | |
public float Damage; | |
private void Update() | |
{ | |
Move(); | |
} | |
/// <summary> | |
/// 控制移动 | |
/// </summary> | |
private void Move() | |
{ | |
this.transform.Translate(Speed * Time.deltaTime, 0, 0); | |
} |
# 随机生成
随机生成敌人(有几个固定的生成位置),增加敌人速度、生成速度,随机生成炸弹箱
固定的生成位置放到一起,统一管理
代码就是几个数学题,比较简单:
private Transform[] AllPositions; // 生成点位 | |
[Tooltip("炸弹箱生成概率 X/100")] | |
public int BomboBoxProbability = 10; | |
[Tooltip("敌人最大生成时间间隔")] | |
public float TimeIntervalMax = 2; | |
[Tooltip("敌人最小生成时间间隔")] | |
public float TimeIntervalMin = 0.5f; | |
[Tooltip("间隔到达最小值需要的时间(秒)")] | |
public float ToMinIntervalTime = 30; | |
[HideInInspector] | |
public float TimeInterval; // 生成时间间隔 | |
private float Timer; // 生成计时器 | |
[Tooltip("敌人最大移动速度(倍率)")] | |
public float MaxSpeed = 3; | |
[HideInInspector] | |
public float CurrentSpeed; // 当前的倍率 | |
[Tooltip("移动速度到达最大值需要的时间(秒)")] | |
public float ToMaxSpeedTime = 60; | |
private void Start() | |
{ | |
// 计算生成点位 | |
AllPositions = new Transform[this.transform.childCount]; | |
for (int i = 0; i < AllPositions.Length; i++) | |
{ | |
AllPositions[i] = this.transform.GetChild(i); | |
} | |
Timer = 0; // 计时器 | |
TimeInterval = TimeIntervalMax; // 生成间隔 | |
CurrentSpeed = 1; // 速度增量 | |
} | |
private void Update() | |
{ | |
Generate(); | |
} | |
/// <summary> | |
/// 生成敌人 | |
/// </summary> | |
private void Generate() | |
{ | |
Timer += Time.deltaTime; | |
// 计算生成间隔 | |
if (TimeInterval > TimeIntervalMin) | |
{ | |
TimeInterval -= ((TimeIntervalMax - TimeIntervalMin) / ToMinIntervalTime) * Time.deltaTime; | |
} | |
else | |
{ | |
TimeInterval = TimeIntervalMin; | |
} | |
// 计算速度增量 | |
if (CurrentSpeed < MaxSpeed) | |
{ | |
CurrentSpeed += MaxSpeed / ToMaxSpeedTime * Time.deltaTime; | |
} | |
else | |
{ | |
CurrentSpeed = MaxSpeed; | |
} | |
if (Timer > TimeInterval) | |
{ | |
// 敌人 | |
int RandomPosition = Random.Range(0, AllPositions.Length); | |
int RandomEnemy = Random.Range(0, EnemyPoolMnager.Intance.AllEnemyPrefabs.Length); | |
GameObject temp = EnemyPoolMnager.Intance.GetEnemy(RandomEnemy, AllPositions[RandomPosition].position); | |
temp.GetComponent<EnemyController>().Speed *= CurrentSpeed; | |
// 炸弹箱 | |
if (TimeInterval == TimeIntervalMin) | |
{ | |
if (Random.Range(0, 100) < BomboBoxProbability) | |
{ | |
BulletPoolMnager.Intance.GetBullet(3, AllPositions[RandomPosition].position + (Vector3.left * 2)); | |
} | |
} | |
Timer = 0; | |
} | |
} |
# 技能
游戏内容较少,可以放到一个脚本里
ID
- 加快攻速:0
- 敌人减速:1
- 炸弹:2
# 计时器
代码很简单:
/// <summary> | |
/// 计时器 | |
/// </summary> | |
/// <param name="coolingTime"></param> | |
private float Timer(float coolingTime) | |
{ | |
if (coolingTime > 0) | |
{ | |
coolingTime -= Time.deltaTime; | |
} | |
if (coolingTime <= 0) | |
{ | |
return 0; | |
} | |
return coolingTime; | |
} | |
/// <summary> | |
/// 处理计时器 | |
/// </summary> | |
/// <param name="CurrentDuration"> 当前持续时间 & lt;/param> | |
/// <param name="CurrentCoolingTime"> 当前冷却时间 & lt;/param> | |
private void UseTimer(ref float CurrentDuration, ref float CurrentCoolingTime) | |
{ | |
// 持续时间 | |
if (CurrentDuration > 0) | |
{ | |
CurrentDuration = Timer(CurrentDuration); | |
} | |
// 冷却时间 | |
if (CurrentCoolingTime > 0) | |
{ | |
CurrentCoolingTime = Timer(CurrentCoolingTime); | |
} | |
} |
# 加快攻速
代码如下:
#region 加快攻速 | |
private bool TurnOn_FireSpeedUp = false; // 开启 | |
[Tooltip("加快攻速(乘算)")] | |
public float FireSpeedUp = 1.5f; | |
[Tooltip("加快攻速持续时间")] | |
public float Duration_FireSpeedUp = 2; | |
[HideInInspector] | |
public float CurrentDuration_FireSpeedUp = 0; // 当前持续时间 | |
[Tooltip("加快攻速冷却时间")] | |
public float CoolingTime_FireSpeedUp = 5; | |
[HideInInspector] | |
public float CurrentCoolingTime_FireSpeedUp = 0; // 当前冷却时间 | |
#endregion | |
/// <summary> | |
/// 加快攻速 | |
/// </summary> | |
private void Skill_FireSpeedUp() | |
{ | |
if (Input.GetKeyDown(KeyCode.Z)) | |
{ | |
if (CurrentCoolingTime_FireSpeedUp == 0) | |
{ | |
CurrentDuration_FireSpeedUp = Duration_FireSpeedUp; | |
CurrentCoolingTime_FireSpeedUp = CoolingTime_FireSpeedUp; | |
this.GetComponent<PlayerFire>().FireSpeed *= FireSpeedUp; | |
TurnOn_FireSpeedUp = true; | |
} | |
} | |
if (TurnOn_FireSpeedUp) | |
{ | |
if (CurrentDuration_FireSpeedUp == 0) | |
{ | |
this.GetComponent<PlayerFire>().FireSpeed /= FireSpeedUp; | |
TurnOn_FireSpeedUp = false; | |
} | |
} | |
UseTimer(ref CurrentDuration_FireSpeedUp, ref CurrentCoolingTime_FireSpeedUp); | |
} |
# 敌人减速
与 加快攻速 相同,代码就不展示了
# 炸弹
使用后回收所有敌人,使用次数 - 1,代码就不展示了
# 碰撞层级忽略
忽略无用的碰撞
代码如下:
//6:House 8:Enemy 9:Player 10:Back 11:Bullet | |
private void Start() | |
{ | |
Physics2D.IgnoreLayerCollision(9, 6); | |
Physics2D.IgnoreLayerCollision(8, 8); | |
Physics2D.IgnoreLayerCollision(8, 9); | |
Physics2D.IgnoreLayerCollision(8, 10); | |
Physics2D.IgnoreLayerCollision(11, 6); | |
Physics2D.IgnoreLayerCollision(11, 9); | |
Physics2D.IgnoreLayerCollision(11, 10); | |
Physics2D.IgnoreLayerCollision(11, 11); | |
} |
# UI
- 显示分数,不展示了
- 显示炸弹使用次数,不展示了
- 显示技能(两个图片,其中一个更暗,表示冷却)
有两个 有冷却时间的技能,所以用 ID 区分
其他的都很简单,不展示了
# 重新开始
初始化所有数值,控制 UI
代码很简单:
[Tooltip("GameOverUI")] | |
public GameObject GameOverUI; | |
[Tooltip("生成器")] | |
public GenerateEnemy GenerateEnemy; | |
[Tooltip("玩家发射子弹")] | |
public PlayerFire PlayerFire; | |
[Tooltip("玩家技能")] | |
public Skill Skill; | |
[Tooltip("血量")] | |
public HouseCollider House; | |
[Tooltip("得分")] | |
public ScoreUI Score; | |
private void Update() | |
{ | |
//Restart | |
if (Input.GetKeyDown(KeyCode.V)) | |
{ | |
GameOverUI.SetActive(false); | |
GenerateEnemy.gameObject.SetActive(true); | |
GenerateEnemy.TimeInterval = GenerateEnemy.TimeIntervalMax; | |
GenerateEnemy.CurrentSpeed = 1; | |
PlayerFire.FireSpeed = 1; | |
PlayerFire.IsFire = true; | |
Skill.CurrentCoolingTime_FireSpeedUp = 0; | |
Skill.CurrentCoolingTime_MoveSpeedDown = 0; | |
Skill.CurrentAvailableTimes_Bomb = Skill.AvailableTimes_Bomb; | |
House.CurrentBlood = House.Blood; | |
House.IsGameOver = false; | |
Score.Score = 0; | |
Score.Timer = 0; | |
RecoveryAll(); | |
} | |
//GameOver | |
if (House.IsGameOver) | |
{ | |
GameOverUI.SetActive(true); | |
PlayerFire.IsFire = false; | |
GenerateEnemy.gameObject.SetActive(false); | |
RecoveryAll(); | |
} | |
} | |
/// <summary> | |
/// 回收所有 敌人 / 子弹 | |
/// </summary> | |
public static void RecoveryAll() | |
{ | |
// 回收所有敌人 | |
foreach (var item in GameObject.FindGameObjectsWithTag("Enemy")) | |
{ | |
EnemyPoolMnager.Intance.RecoveryEnemy(item.GetComponent<EnemyController>().ID, item); | |
} | |
// 回收所有子弹 | |
foreach (var item in GameObject.FindGameObjectsWithTag("Bullet")) | |
{ | |
BulletController temp = item.GetComponent<BulletController>(); | |
if (temp != null) | |
{ | |
BulletPoolMnager.Intance.RecoveryBullet(temp.ID, item); | |
} | |
} | |
// 回收所有炸弹箱 | |
foreach (var item in GameObject.FindGameObjectsWithTag("BombBox")) | |
{ | |
BulletPoolMnager.Intance.RecoveryBullet(item.GetComponent<BombBoxController>().ID, item); | |
} | |
} |
# 大功告成
现在,我们已经完成了这个游戏,是不是非常简单呢?
Then, play and enjoy it.