# 观前提醒
项目会在 GitHub 中开源,链接:https://github.com/Maikire/UnityGameDemo/tree/main/01-02
本次制作的小游戏很适合初学者。
如果有什么问题或想法,欢迎各位在评论区留言。
# 小游戏
点我开始游戏
如上面所展示的,这是一种很常见很简单的小游戏,接下来让我们一步一步地来实现它。
# 角色控制
首先要明确的一点,角色是否可以左右移动?
如果可以,那么摄像机和背景都要跟着动。
如果不可以,哪么只需要处理这些墙体就可以了。本文选择的是:不可以左右移动。这样做起来会更简单。
那么接下来只需要上下移动。组件如下(注:刚体要有重力)
关于动画:需要用到 Animator ,这里没有展示, 可以去看看项目
代码如下:
[Tooltip("向上的速度")] | |
public float Move_Up; | |
private Rigidbody2D Player; | |
// 将这两个属性放到这里面,方便获取 | |
public int Score; // 得分 | |
public bool IsGameOver; // 游戏是否结束 | |
private void Start() | |
{ | |
Player = this.GetComponent<Rigidbody2D>(); | |
Score = 0; | |
IsGameOver = false; | |
} | |
private void Update() | |
{ | |
Jump(); | |
} | |
public void Jump() | |
{ | |
if (!IsGameOver) | |
{ | |
if (Input.GetMouseButtonDown(0)) | |
{ | |
// 施加一个向上的速度 | |
Player.velocity = Vector2.up * Move_Up; | |
} | |
} | |
} |
# 墙体控制
墙体需要向左移动(给玩家一种角色在向右移动的错觉)
墙体需要有两个碰撞器和一个触发器,触发器在中间,角色碰到碰撞器直接游戏结束 ,当角色从触发器中出去的时候增加分数。
代码如下:
/// <summary> | |
/// WallController | |
/// </summary> | |
public class WallController : MonoBehaviour | |
{ | |
[Tooltip("移动速度")] | |
public float MoveSpeed; | |
[HideInInspector] | |
public Vector2 OriginalLocation; | |
[HideInInspector] | |
[Tooltip("true:启用触发器")] | |
public bool IsTriggerActive; | |
private void Start() | |
{ | |
OriginalLocation = this.transform.position; | |
IsTriggerActive = true; | |
} | |
private void Update() | |
{ | |
Move(); | |
} | |
/// <summary> | |
/// 控制移动 | |
/// </summary> | |
private void Move() | |
{ | |
this.transform.Translate(Vector2.left * MoveSpeed * Time.deltaTime); | |
} | |
private void OnCollisionEnter2D(Collision2D collision) | |
{ | |
collision.transform.GetComponent<PlayerController>().IsGameOver = true; | |
} | |
private void OnTriggerExit2D(Collider2D collision) | |
{ | |
if (IsTriggerActive) | |
{ | |
collision.GetComponent<PlayerController>().Score++; | |
} | |
} | |
} |
# 墙体的 “生成”
墙体位置
- X 轴的值:需要确定间隔
- Y 轴的值:使用随机数
- 需要提前在场景中摆好这四个墙体
同样的墙体只需要 4 个,直接将墙体放到一起统一管理
WallManger 的脚本如下
[Tooltip("Y位置上限")] | |
public float PositionMax = 2.4f; | |
[Tooltip("Y位置下限")] | |
public float PositionMin = -2.33f; | |
[Tooltip("位置间隔")] | |
public float Interval = 6; | |
[Tooltip("移出屏幕的偏移量")] | |
public float Offset = 150f; // 当墙体移出屏幕时,应该将它循环利用 | |
[HideInInspector] | |
public Transform[] Walls; | |
private float RandomPosition; //Y 轴随机位置 | |
private Vector3 NewPosition; | |
[HideInInspector] | |
public Transform Last; // 最后一个 | |
[Tooltip("加分出现概率 X/100")] | |
// 这个是可以加 10 分的物体,用触发器触发,下文中会说明 | |
// 挂载的脚本为 DrumstickController | |
// 还有一个池子(实际上就是队列) DrumstickPool | |
public float Probability = 50; | |
private void Start() | |
{ | |
Walls = new Transform[this.transform.childCount]; | |
NewPosition = new Vector3(0, 0, 0); | |
Initialization(); | |
Last = Walls[Walls.Length - 1]; | |
} | |
private void Update() | |
{ | |
GeneratePosition(); | |
} | |
/// <summary> | |
/// 初始化 | |
/// </summary> | |
private void Initialization() | |
{ | |
// 获取所有的 Wall | |
for (int i = 0; i < Walls.Length; i++) | |
{ | |
Walls[i] = this.transform.GetChild(i); | |
} | |
} | |
/// <summary> | |
/// 生成位置 | |
/// </summary> | |
private void GeneratePosition() | |
{ | |
foreach (var item in Walls) | |
{ | |
// 移出屏幕的判定 | |
if (Camera.main.WorldToScreenPoint(item.position).x < -Offset) | |
{ | |
// 重新移动到新位置 | |
RandomPosition = UnityEngine.Random.Range(PositionMin, PositionMax); | |
NewPosition.x = Last.position.x + Interval; | |
NewPosition.y = RandomPosition; | |
item.position = NewPosition; | |
// 加 10 分的物体,概率出现 | |
if (UnityEngine.Random.Range(0, 100) < Probability) | |
{ | |
GameObject temp = DrumstickPool.Intance.GetDrumstick(item.position); | |
temp.GetComponent<DrumstickController>().MoveSpeed = item.GetComponent<WallController>().MoveSpeed; | |
} | |
Last = item; | |
} | |
} | |
} |
# 特殊物体
加 10 分的物体,概率出现,使用触发器,移出屏幕或触发后回收即可,具体代码不多赘述了
主要是如何生成以及回收,这里使用的是池子,它的好处就是循环利用,没有过多生成以及 Destroy () 的性能消耗
它的本质是一个队列,通用性很高。
代码如下:
// 使用这个类的入口,因为只需要一个实例,所以提供一个接口 | |
public static DrumstickPool Intance; | |
[Tooltip("Drumstick")] | |
public GameObject Drumstick; | |
private Queue<GameObject> Pool; // 队列 | |
private void Awake() | |
{ | |
Intance = this; | |
} | |
private void Start() | |
{ | |
Pool = new Queue<GameObject>(); | |
} | |
public GameObject GetDrumstick(Vector2 position) | |
{ | |
if (Pool.Count > 0) | |
{ | |
GameObject temp = Pool.Dequeue(); | |
temp.transform.position = position; | |
temp.SetActive(true); | |
return temp; | |
} | |
else | |
{ | |
return Instantiate(Drumstick, position, Quaternion.identity); | |
} | |
} | |
public void RecoveryDrumstick(GameObject drumstick) | |
{ | |
Pool.Enqueue(drumstick); | |
drumstick.SetActive(false); | |
} |
# 背景
需要一个组件 EdgeCollider2D ,这样就可以使用一条曲线来控制碰撞器的范围,撞到了就游戏结束,代码就不展示了
# 层级碰撞忽略
可以忽略一些无用的碰撞
//9:Player 10:Back 12:Wall | |
private void Awake() | |
{ | |
Physics2D.IgnoreLayerCollision(10, 12); | |
Physics2D.IgnoreLayerCollision(12, 12); | |
} |
# UI
UI 比较简单,只需要三个:显示分数,显示游戏结束(重新开始的按钮),游戏开始的按钮
显示分数很简单,不展示代码了,其他部分请看下文
# 开始游戏、游戏结束、重置游戏
由于游戏的变量较少,可以用一个脚本来控制
功能包括:
- 开始游戏:UI,启动角色,初始化所有数值
- 游戏结束:停止墙的运动,UI
- 重置游戏:初始化所有数值
- 注意:因为使用触发器判断,所以在重置游戏的时候,需要利用协程,让触发器延迟开启
代码如下:
// 有的地方没有注释,直接看名字就知道是什么了 | |
/// <summary> | |
/// ReStart | |
/// </summary> | |
public class ReStart : MonoBehaviour | |
{ | |
[HideInInspector] | |
public bool IsStartGame = false; | |
public GenerateWall WallManger; | |
public PlayerController Player; | |
public WallController Wall; | |
public GameObject GameoverUI; | |
public GameObject StartUI; | |
public GameObject PressUI; | |
private void Update() | |
{ | |
IfGameOver(); | |
} | |
public void StartButton() | |
{ | |
IsStartGame = true; | |
ReStartAll(); | |
Player.gameObject.SetActive(true); | |
PressUI.SetActive(true); | |
StartUI.SetActive(false); | |
} | |
public void ReStartButton() | |
{ | |
ReStartAll(); | |
PressUI.SetActive(true); | |
GameoverUI.SetActive(false); | |
} | |
private void StopAll() | |
{ | |
foreach (var item in WallManger.Walls) | |
{ | |
item.GetComponent<WallController>().MoveSpeed = 0; | |
} | |
foreach (var item in GameObject.FindGameObjectsWithTag("Drumstick")) | |
{ | |
item.GetComponent<DrumstickController>().MoveSpeed = 0; | |
} | |
} | |
private void ReStartAll() | |
{ | |
Player.IsGameOver = false; | |
Player.Score = 0; | |
//Wall | |
foreach (var item in WallManger.Walls) | |
{ | |
WallController wallController = item.GetComponent<WallController>(); | |
wallController.IsTriggerActive = false; | |
wallController.MoveSpeed = Wall.MoveSpeed; | |
item.position = wallController.OriginalLocation; | |
this.StartCoroutine(ResetWallTrigger(wallController)); | |
} | |
WallManger.Last = WallManger.Walls[WallManger.Walls.Length - 1]; | |
//Drumstick | |
foreach (var item in GameObject.FindGameObjectsWithTag("Drumstick")) | |
{ | |
DrumstickPool.Intance.RecoveryDrumstick(item); | |
} | |
//Player | |
Player.transform.position = Vector3.left * 2; | |
} | |
private void IfGameOver() | |
{ | |
if (Player.IsGameOver) | |
{ | |
StopAll(); | |
if (IsStartGame) | |
{ | |
PressUI.SetActive(false); | |
GameoverUI.SetActive(true); | |
} | |
} | |
} | |
/// <summary> | |
/// 延迟开启触发器检测 | |
/// </summary> | |
/// <param name="wallController"></param> | |
/// <returns></returns> | |
private IEnumerator ResetWallTrigger(WallController wallController) | |
{ | |
yield return new WaitForSeconds(0.5f); | |
wallController.IsTriggerActive = true; | |
} | |
} |
# 大功告成
现在,我们已经完成了这个游戏,是不是非常简单呢?
Then, play and enjoy it.