# 观前提醒

项目会在 GitHub 中开源,链接:https://github.com/Maikire/UnityGameDemo/tree/main/01-02

本次制作的小游戏很适合初学者。

如果有什么问题或想法,欢迎各位在评论区留言。

# 小游戏

img

如上面所展示的,这是一种很常见很简单的小游戏,接下来让我们一步一步地来实现它。

# 需求分析

# 角色

  • 上下左右移动,不能移出屏幕
  • 自动攻击
  • 有三个技能
  • 角色没有生命值

# 子弹

  • 一发子弹就能解决敌人
  • 命中时生成爆炸的动画

# 技能

  1. 加快攻击速度,有冷却时间和持续时间,按键:Z
  2. 减慢已经生成的敌人的移动速度,有冷却时间和持续时间,按键:X
  3. 炸弹:延时爆炸,可以清屏,有使用次数,没有冷却时间,按键:C

# 城堡

  • 有生命值,生命值为 0 时结束游戏
  • 受击特效(变红色 + 生成一个爆炸的动画)

# 敌人

  • 有两种,随机生成
  • 移动速度快的攻击力低
  • 移动速度慢的攻击力高
  • 移动速度随游戏时间增加,有上限
  • 生成速度随游戏时间增加,有上限
  • 碰到城堡,减少城堡的生命值

# UI

  • 分数、技能
  • 结束游戏显示 UI
  • 重新开始的按钮

# 其他

  • 弹药箱:敌人移动速度达到最快时,随机生成,增加炸弹使用次数
  • 分数:随游戏时间增加
  • 按 V 键 重置游戏

# 池子

游戏中有几个物体会反复生成和清除,对于这样的情况,应该写一个池子(就是队列)

池子应该分为两种:敌人池,子弹池

# 敌人池

  • 有两种敌人,必须要两个池子。如果只用一个池子,就会失去随机性(杀敌的顺序就是生成敌人的顺序,这显然不是我们想要的)。
  • 再看生成随机的方法,只有 int 类型和 float 类型
  • 所以,ID 就显得尤为重要
  • 游戏内容较少,所以可以使用两个字典集合,一个存储游戏物体(一个 ID 对应一个物体),另一个存储队列(一个 ID 对应一个队列)

代码如下:

EnemyPoolMnagerGitHub链接
//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);
}

# 子弹池

包含子弹、爆炸效果,炸弹,炸弹箱。与敌人池的原理相同,这里不展示了

# 角色控制

组件如下:

img

关于动画:需要用到 Animator ,这里没有展示, 可以去看看项目

# 移动控制:使用 Rigidbody2D 中的 MovePosition

注:如果使用 transform.Translate ,当角色碰到碰撞器时继续向碰撞器移动,就会发生抖动,这不是我们想要的

代码如下:

PlayerControllerGitHub链接
[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);
}

# 自动攻击:自定义攻击速度

代码如下:

PlayerFireGitHub链接
[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;
        }
    }
}

# 城堡

应包含:生命值,判断游戏是否结束,受击特效(在下文中会讲到),改变血条,回收敌人

使用触发器

代码如下:

HouseColliderGitHub链接
[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;
    }
}

# 子弹、炸弹、炸弹箱、爆炸特效

这几个放在一起讲

# 子弹

使用触发器,触发了就生成特效、修改数值。移出屏幕了就回收。

代码如下:

BulletControllerGitHub链接
[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);
    }
}

# 炸弹

代码很简单:

BombControllerGitHub链接
[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,如图:

img

代码很简单:

BulletExplosionBehaviourGitHub链接
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);
    }
}

# 敌人控制

代码很简单:

EnemyControllerGitHub链接
[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);
}

# 随机生成

随机生成敌人(有几个固定的生成位置),增加敌人速度、生成速度,随机生成炸弹箱

固定的生成位置放到一起,统一管理

代码就是几个数学题,比较简单:

GenerateGitHub链接
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,代码就不展示了

# 碰撞层级忽略

忽略无用的碰撞

代码如下:

LayerIgnoreGitHub链接
//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

  1. 显示分数,不展示了
  2. 显示炸弹使用次数,不展示了
  3. 显示技能(两个图片,其中一个更暗,表示冷却)

img

有两个 有冷却时间的技能,所以用 ID 区分

其他的都很简单,不展示了

# 重新开始

初始化所有数值,控制 UI

代码很简单:

RestartGitHub链接
[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.