Unity实战之FlyPin(见缝插针)小游戏的实现
作者:仙魁XAN 时间:2022-05-21 19:46:31
一、简单介绍
Unity 游戏实例开发集合,使用简单易懂的方式,讲解常见游戏的开发实现过程,方便后期类似游戏开发的借鉴和复用。
本节介绍,FlyPin (见缝插针) 休闲小游戏快速实现的方法,希望能帮到你,若有不对,请留言。
二、FlyPin (见缝插针)游戏内容与操作
1、游戏开始,针 Pin 自动准备好,
2、鼠标点击左键发射 Pin,飞向目标,同时自动准备下一根针 Pin,并增加分数
3、针 Pin 插入目标后,会随之一起转动
4、当两个针 Pin 发生碰撞,则游戏结束
5、游戏结束动画完成后,自动重新开始游戏
三、游戏代码框架
四、知识点
1、MonoBehaviour 生命周期函数:Awake,Start,Update,OnGUI
2、Input 按键的监控
3、GameObject.Instantiate 物体的生成,GameObject.Destroy 物体的销毁
4、Camera.orthographicSize 正交相机视口大小修改,Camera.backgroundColor 相机 SolidColor 背景色的修改
5、Rigidbody2D 取消重力效果,添加 Collider ,进行 Trigger 碰撞检测
6、GUIStyle GUI样式,GUI.Label 添加文字的功能使用
7、Vector3.Lerp 位移向量插值使用,Vector3.Distance 位置距离函数的使用
8、Mathf.Lerp ,Color.Lerp 数值和颜色插值计算的使用
9、Transform.Rotate 旋转使用
10、IEnumerator 协程 , StartCoroutine 开始协程 和 StopAllCoroutines 停止所有协程的使用
11、SimpleMessageCenter 简单的消息中心使用
12、Action<int> OnChangeValue 属性变化中委托的使用
13、Resources.Load<GameObject>() 代码加载预制体的使用
14、OnTriggerEnter2D 2D 碰撞检测的使用
15、 SceneManager.LoadScene 加载,和 SceneManager.GetActiveScene() 当前场景的获取
16、等等
五、游戏效果预览
六、实现步骤
这是一个 2D 游戏,主要用到 SpriteRenderer 、2D Collider,2D Rigidbody,以及 TextMesh 等资源组件,所有资源都是Unity自带,没有导入其他外部贴图模型资源。
1、打开 Unity,创建工程
2、构建场景,添加 Pin 初始生成,准备和飞向的地方,以及旋转的目标圆,和显示分数的 3DText
3、ScoreText,创建是 3D Object - 3D Text,其中 根据 TextMesh的字体大小,根据 Character Size 和 Font Size 共同控制
4、如果 TextMesh 看起来很模糊,可以改小 Character Size ,放大 Font Size ,如图
5、TargetCircle 是 SpriteRenderer ,根据需要设置颜色,Sprite 是 Unity 自带的 Knob
说明,同时 TargetCircle 也是 Pin 飞向的目标体
6、PinSpawnPos 、PinReadyPos 是 针 Pin 生成的初始位置,和 准备好位置,PinSpawnPos 、PinReadyPos 下的 Cube 是便于观察位置设定的,确定好位置后,可以删掉或者隐藏 Cube
7、注意 ScoreText 、TargetCircle、PinSpawnPos、PinReadyPos,Position.z 都是 0,为了保证实在同一个平面上,满足2D 游戏要求
8、Main Camera,设置如图,ClearFlags 为 Solid Color ,Background 颜色根据需要设定,Projection 为 Orthographic 正交(2D游戏设置相机),Size 为 5 ,你可以根据自己需要设置
9、最后,效果如图
10、Pin 预制体,包含PinHead 和 PinBody
11、Pin 的 PinHead ,SpriteRenderer 的 Sprite 自带的 Knob, 颜色根据自己需要设定,添加 CircleCollider2D 碰撞体,并且勾选 IsTrigger(避免发生碰撞,产生碰撞效果,不勾选则会有碰撞的物理效果),CircleCollider2D的大小可以根据实际情况调整(一般Unity会自动根据Renderer 大小设置默认大小);添加 Rigidbody2D (发生碰撞的必需条件之一),并且BodyType设置为 Kinematic 可以不受重力下坠(或者 BodyType 为 Dynamic ,把 Gravity Scale 设置 为 0 ,也可以不受重力影响)
12、Pin 中的 PinBody,SpriteRenderer 中 Sprite 自带的 Background ,颜色自选,Order in Layer 默认是 0 ,设置为 -1,是为针插入 TargetCircle 效果,Scale 设置为 (1,15,1),是为了细长如针的效果
13、至于 Pin ,放置在 Resources 文件夹下的Prefabs 文件夹,是为了使用 Unity 中 Resources.Load 代码加载预制体,不需要手动挂载预制体,而 Resources.Load 加载就要求把预制体放在 Resources 文件夹下
14、PinHead 类 监听两个针是否插在靠近位置相撞,相撞则发送游戏结束消息
15、PinHead中的 OnTriggerEnter2D(Collider2D collision) 见监听碰撞进入,碰撞则发送游戏借宿消息
/// <summary>
/// 监听两个针是否插在靠近位置相撞
/// 相撞则发送游戏结束消息
/// </summary>
/// <param name="collision"></param>
private void OnTriggerEnter2D(Collider2D collision)
{
// 两个 PinHead 是否碰撞
if (collision.name.Equals(ConstStr.PIN_HEAD_NAME))
{
// 相撞则发送游戏结束消息
SimpleMessageCenter.Instance.SendMsg(MsgType.GameOver);
}
}
16、Pin 类,管理 针 Pin 的状态和运动
17、Pin 类,Init 初始化函数,得到相关位置和 PinHead 脚本的挂载到 Pin 的 PinHead 物体上
参数说明:
// Pin 准备好的位置
private Vector3 m_PinReadyPos;
// Pin 飞行的目标位置
private Vector3 m_PinFlyTargetPos;
// Pin 插入目标位置的距离间隔
private float m_PinFlyTargetPosDistance;
// Pin 的移动速度
private float m_PinMoveSpeed;
// Pin 飞到目标位置的 Transfrorm
private Transform m_PinTargetParentTrans;
// PinHead
private PinHead m_PinHead;
/// <summary>
/// 初始化函数,得到相关位置和 PinHead
/// </summary>
/// <param name="pinsManager"></param>
/// <param name="pinReadyPos"></param>
/// <param name="flyTargetPos"></param>
/// <param name="flyTargetPosDistance"></param>
/// <param name="pinMoveSpeed"></param>
/// <param name="pinTargetParentTrans"></param>
public void Init(PinsManager pinsManager, Vector3 pinReadyPos, Vector3 flyTargetPos,
float flyTargetPosDistance, float pinMoveSpeed, Transform pinTargetParentTrans) {
m_PinsManager = pinsManager;
m_PinReadyPos = pinReadyPos;
m_PinFlyTargetPos = flyTargetPos;
m_PinFlyTargetPosDistance = flyTargetPosDistance;
m_PinMoveSpeed = pinMoveSpeed;
m_PinTargetParentTrans = pinTargetParentTrans;
// PinHead 子物体添加PinHead脚本
m_PinHead = transform.Find(ConstStr.PIN_HEAD_NAME).gameObject.AddComponent<PinHead>() ;
// 设置状态为准备状态
m_CurPinState = PinState.Readying;
}
18、Pin 类,UpdatePinState(),Pin 的状态监听
/// <summary>
/// 状态监听
/// </summary>
void UpdatePinState() {
switch (CurPinState)
{
case PinState.Idle:
break;
case PinState.Readying:
Readying();
break;
case PinState.ReadyOK:
m_PinsManager.CurPin = this;
break;
case PinState.Fly:
Fly();
m_PinsManager.CurPin = null;
break;
default:
break;
}
}
19、Pin 类,Readying() 正在准备的状态函数,Fly() 飞行目标位置状态,包含 Pin真正的运动(Vector3.Lerp),和满足条件(Vector3.Distance)对应的状态切换(注意:把 Pin 实体至于 TargetCircle 即可以 使 Pin 随 TargetCircle 一起转动 (this.transform.SetParent(m_PinTargetParentTrans);))
/// <summary>
/// 正在准备的状态函数
/// </summary>
void Readying() {
// 移动到准备位置
transform.position = Vector3.Lerp(transform.position, m_PinReadyPos, Time.deltaTime * m_PinMoveSpeed);
// 判断是否到达准备位置
if (Vector3.Distance(transform.position, m_PinReadyPos)<=0.1f)
{
transform.position = m_PinReadyPos;
// 到达后切换状态
CurPinState = PinState.ReadyOK;
}
}
/// <summary>
/// 飞行目标位置状态
/// </summary>
void Fly()
{
// 移动到目标位置
transform.position = Vector3.Lerp(transform.position, m_PinFlyTargetPos, Time.deltaTime * m_PinMoveSpeed);
if (Vector3.Distance(transform.position, m_PinFlyTargetPos) <= m_PinFlyTargetPosDistance)
{
// 到达后切换状态,并且置于 飞到的目标下,使之随目标一起转动
CurPinState = PinState.Idle;
this.transform.SetParent(m_PinTargetParentTrans);
}
}
20、PinsManager Pins 管理类,主要管理 Pin 的生成和 发射 Fly
21、PinsManager Pins 管理类,构造函数 PinsManager(),Resources.Load 获取预制体 Pin 预制体,和参数赋值
参数说明:
// Pin 预制体
private GameObject m_PinPrefab;
// Pin 生成位置
private Vector3 m_PinSpawnPos;
// Pin 准备位置
private Vector3 m_PinReadyPos;
// Pin 飞向目标位置
private Vector3 m_PinFlyTargetPos;
// Pin 飞向目标插入间距
private float m_PinFlyTargetPosDistance;
// Pin 移动速度
private float m_PinMoveSpeed;
// Pin 飞向目标实体
private Transform m_PinTargetParentTrans;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="pinSpawnPos"></param>
/// <param name="pinReadyPos"></param>
/// <param name="flyTargetPos"></param>
/// <param name="flyTargetPosDistance"></param>
/// <param name="pinMoveSpeed"></param>
/// <param name="pinTargetParentTrans"></param>
public PinsManager(Vector3 pinSpawnPos, Vector3 pinReadyPos,Vector3 flyTargetPos,
float flyTargetPosDistance, float pinMoveSpeed,Transform pinTargetParentTrans) {
// 获取预制体
m_PinPrefab = Resources.Load<GameObject>(ConstStr.RESOURCES_PREFABS_PIN_PATH);
// 参数赋值
m_PinSpawnPos = pinSpawnPos;
m_PinReadyPos = pinReadyPos;
m_PinFlyTargetPos = flyTargetPos;
m_PinFlyTargetPosDistance = flyTargetPosDistance;
m_PinMoveSpeed = pinMoveSpeed;
m_PinTargetParentTrans = pinTargetParentTrans;
}
22、PinsManager Pins 管理类,SpawnPin() 根据条件 生成准备的 Pin
/// <summary>
/// 生成准备的 Pin
/// </summary>
public void SpawnPin() {
// 生成准备的 Pin
if (m_PinPrefab!=null && m_ReadyPin == null)
{
GameObject pinGo = GameObject.Instantiate(m_PinPrefab);
// 设置生成位置
pinGo.transform.position = m_PinSpawnPos;
// 添加 Pin 脚本
m_ReadyPin = pinGo.AddComponent<Pin>();
// Pin 脚本 初始化
m_ReadyPin.Init(this,m_PinReadyPos,m_PinFlyTargetPos,
m_PinFlyTargetPosDistance,m_PinMoveSpeed,m_PinTargetParentTrans);
// 添加到集合中
m_PinsList.Add(pinGo);
}
}
23、PinsManager Pins 管理类,FlyPin() 根据条件 让当前 Pin 飞向目标位置,返回是否有可飞行的 Pin,有则 true,DestroyAllPins() 销毁清空 Pins 集合
/// <summary>
/// 让当前 Pin 飞向目标位置
/// 返回是否有可飞行的 Pin
/// </summary>
/// <returns>true : 有可飞行 Pin </returns>
public bool FlyPin() {
if (CurPin!=null)
{
m_ReadyPin = null;
if (CurPin.CurPinState==PinState.ReadyOK)
{
CurPin.CurPinState = PinState.Fly;
}
return true;
}
return false;
}
/// <summary>
/// 销毁清空 Pins 集合
/// </summary>
public void DestroyAllPins() {
for (int i=m_PinsList.Count-1; i >= 0 ; i--)
{
GameObject.Destroy(m_PinsList[i]);
}
m_PinsList.Clear();
}
24、TargetCircleManager Pin 飞向目标圆管理类
25、TargetCircleManager Pin 飞向目标圆管理,TargetCircleManager()构造函数 获取设置 旋转的实体和旋转速度
参数说明:
// 实体Transform
Transform m_TargetCircleTrans;
// 转动速度
float m_Speed;
// 是否开始转动
bool m_IsRotated = false;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="target">目标实体</param>
/// <param name="rotSpeed">旋转速度</param>
public TargetCircleManager(Transform target, float rotSpeed) {
m_TargetCircleTrans = target;
m_Speed = rotSpeed;
m_IsRotated = false;
}
26、TargetCircleManager Pin 飞向目标圆管理, void UpdateRotateSelf() 更新自身旋转,StartRotateSelf() 开始旋转,StopRotateSelf() 停止旋转
/// <summary>
/// 更新自身旋转
/// </summary>
public void UpdateRotateSelf() {
if (m_TargetCircleTrans != null && m_IsRotated==true)
{
m_TargetCircleTrans.Rotate(Vector3.forward,Time.deltaTime * m_Speed * -1); // -1 是让其反向旋转
}
}
/// <summary>
/// 开始旋转
/// </summary>
public void StartRotateSelf()
{
m_IsRotated = true;
}
/// <summary>
/// 停止旋转
/// </summary>
public void StopRotateSelf() {
m_IsRotated = false;
}
27、ScoreManager 分数管理类,管理分数,和分数变化更新的委托事件
public int Score { get { return m_Scroe; }
set {
// 判断分数是否更新,更新则触发更新事件
if (m_Scroe!=value)
{
m_Scroe = value;
if (OnChangeValue!=null)
{
OnChangeValue.Invoke(value);
}
}
}
}
// 分数变化委托
public Action<int> OnChangeValue;
28、SimpleMessageCenter 简单消息中心,管理消息的注册,触发,和清空,并且设置静态单例(static SimpleMessageCenter Instance),方便被访问使用
29、SimpleMessageCenter 简单消息中心, RegisterMsg()注册消息,SendMsg()发送消息,ClearAllMsg() 清空消息
/// <summary>
/// 注册消息
/// </summary>
/// <param name="msgType"></param>
/// <param name="action"></param>
public void RegisterMsg(MsgType msgType, Action action) {
if (m_MsgDict.ContainsKey(msgType) == true)
{
m_MsgDict[msgType] += action;
}
else {
m_MsgDict.Add(msgType,action);
}
}
/// <summary>
/// 发送消息
/// </summary>
/// <param name="msgType"></param>
public void SendMsg(MsgType msgType)
{
if (m_MsgDict.ContainsKey(msgType) == true)
{
m_MsgDict[msgType].Invoke();
}
}
/// <summary>
/// 清空消息
/// </summary>
public void ClearAllMsg() {
m_MsgDict.Clear();
}
30、ConstStr 统一管理一些不可变的常量字符串
/// <summary>
/// 不可变字符串
/// </summary>
public class ConstStr
{
// Pin 飞行目标圆 名字路径
public const string WORLD_TARGETCIRCLE_NAME_PATH = "World/TargetCircle";
// Pin Resources 预制体路径
public const string RESOURCES_PREFABS_PIN_PATH = "Prefabs/Pin";
// Pin Head 名字
public const string PIN_HEAD_NAME = "PinHead";
}
31、Enum 统一管理枚举类型
/// <summary>
/// Pin 状态
/// </summary>
public enum PinState
{
Idle = 0,// 闲置状态
Readying,// 正在准备状态
ReadyOK,// 装备好状态
Fly,// 飞向目标状态
}
/// <summary>
/// 消息类型
/// </summary>
public enum MsgType
{
GameOver = 0, // 游戏结束
}
32、GameManager 游戏管理类 ,直接挂载到场景中,获取一些场景游戏实体,并管理一些游戏逻辑的初始化等
33、GameManager 游戏管理类 ,Awake() 和Start() Unity自带函数,初始化变量和类,以及启动游戏,TargetCircle 开始旋转,Pin 开始准备,在简单消息中心注册监听游戏结束事件,设置 分数变化更新到 TextMesh 上,等等
private void Awake()
{
// 初始化参数值
m_MainCamera = Camera.main;
m_IsGameOver = false;
Transform targetCircleTrans = GameObject.Find(ConstStr.WORLD_TARGETCIRCLE_NAME_PATH).transform;
m_TargetCircleManager = new TargetCircleManager(targetCircleTrans, RotateSpeed);
m_PinsManager = new PinsManager(m_PinSpawnPos.position,m_PinReadyPos.position, targetCircleTrans.position,
m_PinFlyTargetPosDistance,m_PinMoveSpeed,targetCircleTrans);
m_ScoreManager = new ScoreManager();
}
private void Start()
{
// 开始TargetCircle旋转
m_TargetCircleManager.StartRotateSelf();
// 生成第一个 Pin
m_PinsManager.SpawnPin();
// 注册监听游戏结束消息
SimpleMessageCenter.Instance.RegisterMsg(MsgType.GameOver,ToGameOver);
// 初始化分数 0
m_ScoreManager.Score = 0;
// 分数更新事件,更新 UI
m_ScoreManager.OnChangeValue += (score)=> { ScoreText.text = score.ToString(); };
}
34、GameManager 游戏管理类 ,Update() Unity自带函数,游戏未结束,TargetCircle 每帧转动,并监听鼠标左键按下,Fly Pin 和生成下一个 Pin 准备
private void Update()
{
// 游戏结束
if (m_IsGameOver == true)
{
return;
}
// TargetCircle 更新旋转
m_TargetCircleManager.UpdateRotateSelf();
// 监听鼠标左键按下
if (Input.GetMouseButtonDown(0))
{
// Pin 飞向目标,则增加分数,并且生成下一个 Pin
bool isFly = m_PinsManager.FlyPin();
if (isFly==true)
{
m_ScoreManager.Score++;
m_PinsManager.SpawnPin(); // 生成下一个
}
}
}
35、GameManager 游戏管理类 ,OnDestroy() 在游戏重新加载销毁时,进行一些数据清理置空,并停止可能的所有协程(不阻碍主程运行的小程序),OnGUI() 做一些游戏操作说明
private void OnDestroy()
{
// 销毁所有 Pin
m_PinsManager.DestroyAllPins();
// 清空消息中心
SimpleMessageCenter.Instance.ClearAllMsg();
// 置空分数更新事件
m_ScoreManager.OnChangeValue = null;
// 置空相关参数
m_PinsManager = null;
m_TargetCircleManager = null;
m_ScoreManager = null;
// 停止所有协程
StopAllCoroutines();
}
// Unity 周期函数 每帧调用
private void OnGUI()
{
// 游戏操作说明
GUIStyle fontStyle = new GUIStyle();
fontStyle.normal.background = null; //设置背景填充
fontStyle.normal.textColor = new Color(1, 0, 0); //设置字体颜色
fontStyle.fontSize = 40; //字体大小
GUI.Label(new Rect(10, 10, 200, 200),
"操作说明:\n1、点击鼠标左键发射球体;\n2、两针 Pin 碰撞会自动触发重新开始游戏;",
fontStyle);
}
36、GameManager 游戏管理类 ,ToGameOver()游戏结束事件,停止旋转,开启协程,进行游戏结束特效处理,然后自动重新加载当前场景,重新开始游戏
/// <summary>
/// 游戏结束
/// </summary>
void ToGameOver() {
// 游戏结束
if (m_IsGameOver==true)
{
return;
}
// 游戏结束
m_IsGameOver = true;
// 停止目标旋转
m_TargetCircleManager.StopRotateSelf();
// 开始结束协程
StartCoroutine(GameOver());
}
/// <summary>
/// 游戏结束协程
/// </summary>
/// <returns></returns>
IEnumerator GameOver() {
while (true)
{
// 等待帧最后
yield return new WaitForEndOfFrame();
// 更新主Camera 视口
m_MainCamera.orthographicSize = Mathf.Lerp(m_MainCamera.orthographicSize, m_OrthographicSize,Time.deltaTime * 10);
// 更新主Camera 背景色
m_MainCamera.backgroundColor = Color.Lerp(m_MainCamera.backgroundColor, Color.red,Time.deltaTime * 5);
// 更新主Camera 视口 到位,跳出循环
if ((m_MainCamera.orthographicSize - m_OrthographicSize)<0.01f)
{
break;
}
}
// 加载当前场景
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
37、把 GameManager 挂载到场景中,根据 GameManager 参数说明赋值,一些速度等可以根据实际需要修改
38、运行场景, Pin 自动准备好,游戏就开始了
39、点击鼠标左键,即可发射Fly,如图
七、工程源码地址
github 地址:GitHub - XANkui/UnityMiniGameParadise: Unity 游戏开发集合代码集
八、延伸扩展
游戏的好不好玩,趣味性,视觉化等诸多因素影响,下面简单介绍几个方面拓展游戏的方向,仅做参考
1、可以根据自己需要修改游戏资源,换肤什么的等
2、可以根据需要添加撞击特效,音效等
3、添加 UI 面板等,美化游戏
4、等等
来源:https://blog.csdn.net/u014361280/article/details/122349777