【游戏开发实战】使用Unity 2019制作仿微信小游戏飞机大战(二):搭建基础游戏框架

零、教程目录

使用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

封装一下界面管理器,提供两个接口:ShowPanelHidePanel

// 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窗口,锁定其中一个。
在这里插入图片描述
然后给PrefabSlotItems设置Size1,因为我们只需绑定一个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我们留到下一篇中讲。

六、下篇预告

战斗界面。

  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林新发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值