文章目录
零、教程目录
使用Unity 2019制作仿微信小游戏飞机大战教程已完结。
文章目录如下:
《第一篇:开始游戏界面》
《第二篇:搭建基础游戏框架》
《第三篇:战斗界面UI》
《第四篇:主角飞机序列帧动画》
《第五篇:主角飞机的飞行控制》
《第六篇:根据配置随机生成敌机》
《第七篇:主角飞机碰撞与爆炸》
《第八篇:主角飞机开炮》
《第九篇:敌机受击与爆炸》
《第十篇:敌机血量与得分》
《第十一篇:核弹掉落与全屏炸机》
《第十二篇:敌机开炮》
《第十三篇:游戏暂停、结束与重新开始》
一、前言
嗨,大家好,我是新发。相信很多人玩过微信小游戏经典的飞机大战,如下:
想重温或体验微信这款经典的飞机大战的同学可以点这里:https://gamemaker.weixin.qq.com/ide#/
在网上已经有一些人已经出了Unity
的制作教程,但是比较陈旧,里面使用了已经弃用的组件和写法,用了很陈旧的NGUI
版本,如果使用Unity 2019
或以上版本打开会各种报错,对新入门Unity
的同学不大友好。
于是,我决定写一个全新的教程:《使用Unity2019制作仿微信小游戏飞机大战》,会使用最新的写法,并且使用尽量简洁的设计与代码来完成。
本教程的工程已上传到Github
,感兴趣的同学自行下载学习。
喜欢的同学记得给个星星~
Github
地址:https://github.com/linxinfa/UnityAircraftFight
对Unity
游戏开发有任何问题的,都欢迎在评论区留言,我都会看到的,并会进行认真解答,希望能帮助到想学Unity
开发的同学,共勉。
二、本篇目标
搭建基础游戏框架。本篇可能相对枯燥一些,注意都是代码,希望大家耐心阅读。
三、基础游戏框架
在写业务逻辑之前,我们需要先把基础游戏框架搭建一下,有了框架,就有了规范,业务逻辑才不会乱。
简单的框架如下:
新建文件夹Scripts/Framework
,用来存放我们的框架代码。
1、资源管理器:ResourceMgr.cs
资源管理器主要职责就是加载资源、缓存资源。比如界面、飞机、子弹等等预设资源的加载。
资源管理器代码如下:
// ResourceMgr.cs
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 资源管理器
/// </summary>
public class ResourceMgr
{
/// <summary>
/// 资源缓存
/// </summary>
Dictionary<string, Object> m_res = new Dictionary<string, Object>();
/// <summary>
/// 加载资源
/// </summary>
/// <typeparam name="T">资源类型</typeparam>
/// <param name="resPath">资源路径</param>
/// <returns></returns>
public T LoadRes<T>(string resPath) where T : Object
{
if(m_res.ContainsKey(resPath))
{
return m_res[resPath] as T;
}
T t = Resources.Load<T>(resPath);
m_res[resPath] = t;
return t;
}
// 单例模式
private static ResourceMgr s_instance;
public static ResourceMgr instance
{
get
{
if (null == s_instance)
s_instance = new ResourceMgr();
return s_instance;
}
}
}
2、界面管理器
界面管理器的主要职责就是显示界面和关闭界面。
2.1、界面基类:BasePanel.cs
不同界面的显示与隐藏的逻辑是相同的,不同的是界面内部的UI
逻辑,我们可以封装一个界面基类:BasePanel
,把界面的基本逻辑写在基类中。
// BasePanel.cs
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 界面基类
/// </summary>
public class BasePanel : MonoBehaviour
{
/// <summary>
/// 界面预设资源路径
/// </summary>
protected string m_panelResPath;
/// <summary>
/// 界面GaemObject
/// </summary>
protected GameObject m_panelObj;
/// <summary>
/// 界面父节点
/// </summary>
protected Transform m_panelParent;
public virtual void Awake()
{
m_panelParent = transform;
}
/// <summary>
/// 显示界面
/// </summary>
public virtual void Show()
{
if (null == m_panelObj)
{
var prefab = ResourceMgr.instance.LoadRes<GameObject>(m_panelResPath);
m_panelObj = Instantiate(prefab);
m_panelObj.transform.SetParent(m_panelParent, false);
}
var slot = m_panelObj.GetComponent<PrefabSlot>();
SetUi(slot);
OnShow();
}
/// <summary>
/// 关闭界面
/// </summary>
public virtual void Hide()
{
if (null != m_panelObj)
{
Object.Destroy(m_panelObj);
this.enabled = false;
}
OnHide();
}
protected virtual void OnShow()
{
}
protected virtual void OnHide()
{
}
/// <summary>
/// 设置界面ui逻辑
/// </summary>
public virtual void SetUi(PrefabSlot slot) { }
}
2.2、UI预设插槽:PrefabSlot.cs
BasePanel
脚本中的PrefabSlot
类,是用于绑定界面UI
对象的插槽
,这个思想是源于QT
的信号槽,关于PrefabSlot
可以参见我之前写的这篇文章:《Unity界面预设绑定ui元素(PrefabSlotEditor)》
// PrefabSlot.cs
using UnityEngine;
using System;
using UnityEngine.UI;
/// <summary>
/// 预设插槽
/// </summary>
public class PrefabSlot : MonoBehaviour
{
[Serializable]
public class Item
{
public string name;
public UnityEngine.Object obj;
}
public Item[] items = new Item[0];
public UnityEngine.Object GetObj(string name)
{
if (string.IsNullOrEmpty(name)) return null;
for (int i = 0, cnt = items.Length; i < cnt; i++)
{
Item item = items[i];
if (item.name.Equals(name))
{
return item.obj;
}
}
return null;
}
public T GetObj<T>(string name) where T : UnityEngine.Object
{
try
{
return (T)GetObj(name);
}
catch (Exception e)
{
Debug.LogError("PrefabSlot GetObj name = " + name);
return default(T);
}
}
#region 设置ui接口
public Text SetText(string name, string textStr)
{
var text = GetObj<Text>(name);
if (null != text)
{
text.text = textStr;
}
else
{
Debug.LogError("PrefabSlot SetText Error, obj is null: " + name);
}
return text;
}
public Button SetButton(string name, Action<GameObject> onClick)
{
var btn = GetObj<Button>(name);
if (null != btn)
{
btn.onClick.AddListener(() =>
{
onClick(btn.gameObject);
});
}
else
{
Debug.LogError("PrefabSlot SetButton Error, obj is null: " + name);
}
return btn;
}
///TODO: 其他接口可自行拓展
#endregion 设置ui接口
}
2.3、界面管理器:PanelMgr.cs
封装一下界面管理器,提供两个接口:ShowPanel
、HidePanel
。
// PanelMgr.cs
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 界面管理器
/// </summary>
public class PanelMgr
{
private void Init()
{
m_canvasTransform = GameObject.Find("Canvas").transform;
m_isInit = true;
}
/// <summary>
/// 显示界面
/// </summary>
/// <typeparam name="T">界面类</typeparam>
public T ShowPanel<T>() where T : BasePanel
{
if (!m_isInit) Init();
BasePanel panel = null;
var panelName = typeof(T).ToString();
if (m_panels.ContainsKey(panelName))
{
panel = m_panels[panelName];
}
else
{
var panelObj = new GameObject(panelName);
panelObj.transform.SetParent(m_canvasTransform, false);
// 铺满全屏
var rect = panelObj.AddComponent<RectTransform>();
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, Screen.width);
rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, Screen.height);
panel = panelObj.AddComponent<T>();
m_panels[panelName] = panel;
}
panel.enabled = true;
panel.Show();
return panel as T;
}
/// <summary>
/// 关闭界面
/// </summary>
/// <typeparam name="T">界面类</typeparam>
public void HidePanel<T>() where T : BasePanel
{
BasePanel panel = null;
var panelName = typeof(T).ToString();
if (m_panels.ContainsKey(panelName))
{
panel = m_panels[panelName];
}
if (null != panel)
panel.Hide();
}
private Dictionary<string, BasePanel> m_panels = new Dictionary<string, BasePanel>();
private bool m_isInit = false;
private Transform m_canvasTransform;
// 单例模式
private static PanelMgr s_instance;
public static PanelMgr instance
{
get
{
if (null == s_instance)
s_instance = new PanelMgr();
return s_instance;
}
}
}
3、事件管理器:EventDispatcher.cs
事件管理器的思想是观察者模式,事件的发出者不需要关心事件订阅者是谁,只需抛出事件,事件订阅者接收到事件后处理相应的逻辑。这样的好处之一是可以降低模块之间的耦合。
// EventDispatcher.cs
using UnityEngine;
using System.Collections.Generic;
public delegate void MyEventHandler(params object[] objs);
/// <summary>
/// 事件管理器,订阅事件与事件触发
/// </summary>
public class EventDispatcher
{
/// <summary>
/// 订阅事件
/// </summary>
public void Regist(string eventName, MyEventHandler handler)
{
if (handler == null)
return;
if (!listeners.ContainsKey(eventName))
{
listeners.Add(eventName, new Dictionary<int, MyEventHandler>());
}
var handlerDic = listeners[eventName];
var handlerHash = handler.GetHashCode();
if (handlerDic.ContainsKey(handlerHash))
{
handlerDic.Remove(handlerHash);
}
listeners[eventName].Add(handler.GetHashCode(), handler);
}
/// <summary>
/// 注销事件
/// </summary>
public void UnRegist(string eventName, MyEventHandler handler)
{
if (handler == null)
return;
if (listeners.ContainsKey(eventName))
{
listeners[eventName].Remove(handler.GetHashCode());
if (null == listeners[eventName] || 0 == listeners[eventName].Count)
{
listeners.Remove(eventName);
}
}
}
/// <summary>
/// 触发事件
/// </summary>
public void DispatchEvent(string eventName, params object[] objs)
{
if (listeners.ContainsKey(eventName))
{
var handlerDic = listeners[eventName];
if (handlerDic != null && 0 < handlerDic.Count)
{
var dic = new Dictionary<int, MyEventHandler>(handlerDic);
foreach (var f in dic.Values)
{
try
{
f(objs);
}
catch (System.Exception ex)
{
Debug.LogErrorFormat(szErrorMessage, eventName, ex.Message, ex.StackTrace);
}
}
}
}
}
/// <summary>
/// 清空事件
/// </summary>
/// <param name="key"></param>
public void ClearEvents(string eventName)
{
if (listeners.ContainsKey(eventName))
{
listeners.Remove(eventName);
}
}
private Dictionary<string, Dictionary<int, MyEventHandler>> listeners = new Dictionary<string, Dictionary<int, MyEventHandler>>();
private readonly string szErrorMessage = "DispatchEvent Error, Event:{0}, Error:{1}, {2}";
private static EventDispatcher s_instance;
public static EventDispatcher instance
{
get
{
if (null == s_instance)
s_instance = new EventDispatcher();
return s_instance;
}
}
}
4、游戏管理器:GameMgr.cs
游戏管理器的主要职责就是游戏状态和游戏控制。
我们先写下简单的游戏状态和控制。
// GameMgr.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 游戏管理器
/// </summary>
public class GameMgr
{
/// <summary>
/// 游戏主入口函数
/// </summary>
public void Main()
{
gameState = GameState.Ready;
// TODO:显示游戏开始界面
}
public void Update()
{
}
/// <summary>
/// 开始游戏
/// </summary>
public void StartGame()
{
// TODO:关闭开始游戏界面
gameState = GameState.Playing;
// TODO:显示游戏战斗界面
}
/// <summary>
/// 游戏状态
/// </summary>
public GameState gameState = GameState.Ready;
// 单例模式
private static GameMgr s_instance;
public static GameMgr instance
{
get
{
if (null == s_instance)
s_instance = new GameMgr();
return s_instance;
}
}
}
/// <summary>
/// 游戏状态
/// </summary>
public enum GameState
{
Ready,
Playing,
Pause,
End,
}
注意,脚本中我留了几个TODO
,下文中会进行补充。
四、游戏入口脚本:Main.cs
写过C/C++
程序的同学知道,程序会有一个Main
函数作为程序入口函数。
同理,游戏启动时,我们也要搞一个入口脚本:Main.cs
。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 入口脚本
/// </summary>
public class Main : MonoBehaviour
{
/// <summary>
/// 游戏主入口
/// </summary>
void Start()
{
// 调用游戏管理器的Main函数
GameMgr.instance.Main();
}
void Update()
{
GameMgr.instance.Update();
}
}
将Main.cs
挂到Main Camera
上。
五、开始游戏界面逻辑
有了上面的基础框架,我们就可以开始写界面逻辑了。
1、绑定UI对象
在写界面逻辑之前,我们先要给界面绑定UI
对象槽。
给StartGamePnale
预设挂上PrefabSlot
组件。
接下来就可以将界面中的UI
对象绑定到PrefabSlot
中了,比如我们要把界面中的开始游戏按钮绑定过来,这里需要一点技巧。打开两个Inspector
窗口,锁定其中一个。
然后给PrefabSlot
的Items
设置Size
为1
,因为我们只需绑定一个UI
对象,如果是复杂的界面,则可能需要绑定十几个UI
对象。
选中开始游戏按钮,把Button
组件对象拖到Obj
槽中,并命名为StartGameBtn
。
这样,我们就可以通过名字StartGameBtn
获取到开始游戏按钮的Button
组件对象了。
2、开始游戏界面:StartGamePanel.cs
现在,我们可以开始写开始游戏界面的界面代码了,创建Scripts/Panels
文件夹,用来存放界面脚本。
开始界面脚本我们命名为StartGamePanel
。
代码很简单,主要就是点击开始游戏按钮时抛出一个开始游戏的事件。
// StartGamePanel.cs
/// <summary>
/// 开始游戏界面
/// </summary>
public class StartGamePanel : BasePanel
{
public override void Awake()
{
base.Awake();
m_panelResPath = "Panels/StartGamePanel";
}
public override void SetUi(PrefabSlot slot)
{
base.SetUi(slot);
// 开始游戏按钮
slot.SetButton("StartGameBtn", (btn) =>
{
GameMgr.instance.StartGame();
});
}
}
回到GameMgr.cs
脚本,我们之前留的TODO
可以进行处理了。
// GameMgr.cs
/// <summary>
/// 游戏主入口函数
/// </summary>
public void Main()
{
gameState = GameState.Ready;
// 显示游戏开始界面
PanelMgr.instance.ShowPanel<StartGamePanel>();
}
/// <summary>
/// 开始游戏事件 响应函数
/// </summary>
/// <param name="args"></param>
private void OnEventStartGame(params object[] args)
{
// 关闭开始游戏界面
PanelMgr.instance.HidePanel<StartGamePanel>();
gameState = GameState.Playing;
// TODO:显示游戏战斗界面
}
因为游戏战斗界面我们还没做,所以这个TODO
我们留到下一篇中讲。
六、下篇预告
战斗界面。