unity 简单通用游戏模式设计

好吧好吧,又谈到这个问题了,其实早就想写这个博客了,犹豫了好久。在设计游戏的时候我本人是很排斥什么游戏架构设计,mvc什么的,我只想马上动手就把自己的游戏玩法最快的用代码敲出来,还不会出无法挽回的错误,那么下面的步骤可以帮助你构建一个简单的游戏模式架构。

一。首先是数据存储类,如GameManager,UIManager,SoundManager等这些类,这些类是不销毁的,由于这些类为物体组件,用普通的单例模式容易出现实例化的冲突,因此可以首先加载一个数据场景,间隔一段时间后加载第二个正式场景,以后最多的返回也只能返回到第二场景,这样就解决了冲突问题

二。分场景控制类,每个场景中有可能要进行不同的复杂的控制,比如播放个动画,选择角色什么的。比如UI场景,该场景中主要以UI功能为主,设置音量,选择角色什么的,都在该场景中进行,因此可以用个UIScene类的做一些特殊的控制;正式游戏场景,比如在UI场景选择好了敌人,那么现在进入正式打击敌人,冒险什么的,可以建立一个PlayScene场景,该场景用来控制播放个动画次序什么的各种操作。

三。通用类的作用,比如UI控制中物体的隐藏与出现,跳转到另一个场景的操作,销毁物体的操作这些可以用一个GeneralController的集成这些函数,然后作为组件的形式添加到需要的分场景的场景控制的物体上,这一点尤其对UGUI的添加事件很有效。

四。观察者类与数据存储类的静态变量。比如游戏加载后要把当前的语言或者音量设置到相关物体上,那么寻找携带这些数据的Manange类的方式有自带的Find相关类;或者使用数据存储类的instance静态变量,判断该静态变量是否为真,如果为真,那么就说明该类的物体存在,直接调用该静态变量获取到相关需要的数据。因此相比较而言,使用instance静态变量更加可靠高效

五。查找多个物体的问题,为多个物体设置数据。这里涉及到在场景中寻找具有相同标签或者拥有相同组件的物体,这里最好的方式是为每个相同组件的物体添加一个观察者组件,该组件用于当物体生成时,调用相关的管理类,将自己存放到管理类的存储列表中,这样管理类就不用在场景中使用FInd方法去寻找这些物体了。

六。对象池。这里并非这篇博客主要内容,只是做个提醒,因为抛开性能问题,上面的的五条对一般游戏来说足够了。对一些长期生成的物体如子弹等最好做成一个对象池,从而提高机器性能。

七。托管程序。这里的托管和计算机语言的概念相类似。比如现在写一个类,这个类执行一段程序,将物体A移动到位置p1,结束后开启UI界面,选择几个物品,然后关闭UI,让物体A移动到p2,然后制造一场爆炸,过程结束。这个过程就好像做某个任务的过程,如果写这个过程那么移动的过程需要在update函数中执行,爆炸的过程也需要开启,但是如果都在update中执行和判断会导致整个程序相当臃肿。这里我们采用几种方式:第一种,托管和事件,即将如需要每帧执行的函数,就托管给某一个类,成为这个类执行协程。第二种,状态机与事件协同控制。在update函数中有两个状态标识符。state是最外层的状态,为0时update空状态,为1时,判断第二个标识符号modelState,根据modelState的值开启相应的过程,并且同时让state的值为2或者为0,如果开启的过程执行结束,那么可以为该开启的过程添加一个事件,用于让state为1,然后开启另一个过程 。这写过程也可以托管给其他程序执行。

 ===================================================================================================================================================

下面将用实例对上面的模式做出解释,用我的游戏Recoil作为实例解释,这里只是介绍技术,至于游戏的设计呢就不分享了,这个游戏正在做第二个版本的开发:

https://store.steampowered.com/app/844520/RECOIL/

一。数据存储类

这里用一个场景来作为数据读取场景,或者说这是个首页场景,由于这三个类是单例,所以可以根据自己的需要设计。

二。分场景控制类

分场景控制类,或者可以称为该场景下的总控类,控制该场景下的一些除了UI外的特殊必要功能,如临时数据存储,场景状态控制,生成玩家,敌人等,尤其做demo的时候,这些类的作用相当高效,可以代替数据存储类,建立测试用数据,供给UI功能或者其他功能使用;当然如果一个场景功能较多,如关卡控制,流程控制,对象池等,为了避免臃肿可以分作几个类同时挂载在同一个物体上。

三。通用功能类设计

例如播放按钮声音,进行语言设置等,如果此时都放在UImanager中,那么这时在相应场景下,就需要做判断UImanger的相关调用,并且其他场景中又有同样的功能,所以此时可以用在不同的场景控制物体下放置同样功能的类是很有必要的,这里可以在上面看到GeneralController类的挂载物体。

四。UI功能与事件

这里之所以将UI功能与事件搭配,这两个概念总是息息相关。这里对这个概念的理解,要感谢给了我巨大帮助的严xiang,严大佬,学习模仿他的编码风格和编程方式改善了我长期对游戏逻辑控制上的弊病。

首先是UI功能,比如某个UI功能模块,这个模块包括UI和UI对应的功能,比如一个panel下,有多个按钮,图片等,我们首先建立一个空物体作为该UI功能的顶级物体,空物体下放一个panel作为父物体,panel下放置其他按钮,zipanel等,脚本挂载在顶级空物体上,脚本包含两个最主要功能,Open和Close,最简单如Open下panel.SetActive(false),就可以在不影响顶级物体的情况下关闭该UI界面,如果需要更复杂的关闭动画或者打开动画,可以在open和close下继续添加如dotween动画等,如果有必要可以为打开结束或者关闭结束添加相应的事件(如调用打开其他UI等)。当然这点有点像MVC控制模式,下面是一个最简单游戏设置功能:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using UnityEngine.Audio;

public class OptionPanel : MonoBehaviour
{
    [SerializeField]
    GameObject panel;
    [SerializeField]
    MainPanel mainPanel;

    [SerializeField]
    AudioMixer mixer;
    [SerializeField]
    Slider musicSlider;
    [SerializeField]
    Slider effectSlider;

    [SerializeField]
    List<string> languageNames;
    int languageIndex;
    [SerializeField]
    Text langageText;


    int state = 0;
    private void Update()
    {
        if (state == 1)
        {
            if (Input.GetKeyDown(KeyCode.Escape))
            {
                BackButton();
                if (GeneralController.instance) GeneralController.instance.PlayClickAudio();
            }
        }
    }

    public void Open()
    {
        panel.SetActive(true);

        //获取当前音量
        float valueTemp = 0;
        mixer.GetFloat("Music", out valueTemp);
        musicSlider.value = valueTemp;

        mixer.GetFloat("Effect", out valueTemp);
        effectSlider.value = valueTemp;

        //获取当前语言
        if (UIManager.instance)
        {
            List<string> lNames = UIManager.instance.GetLanguageNames();
            if (lNames != null && lNames.Count > 0) languageNames = lNames;
        }
        string name = UIManager.instance.languageManager.GetLanguage();
        languageIndex = languageNames.IndexOf(name);
        langageText.text = UIManager.instance.languageManager.GetCurrentLanguageLocalName();

        StartCoroutine(SetState(1));
    }

    public void Close()
    {
        panel.SetActive(false);
        state = 0;
    }

    IEnumerator SetState(int _state)
    {

        yield return Time.deltaTime;
        state = _state;
        yield break;
    }

    public void LeftLanguageButton()
    {
        SetLanguage(false);
    }

    public void RightLanguageButton()
    {
        SetLanguage(true);
    }

    void SetLanguage(bool isAddSet)
    {
        if (isAddSet)
        {
            languageIndex = (languageIndex + 1) % languageNames.Count;
        }
        else
        {
            languageIndex = languageIndex - 1 >= 0 ? (languageIndex - 1 <= languageNames.Count - 1 ? languageIndex - 1 : languageNames.Count - 1) : languageNames.Count - 1;
        }
        UIManager.instance.SetLanguage(languageNames[languageIndex]);
        langageText.text = UIManager.instance.languageManager.GetCurrentLanguageLocalName();
        GeneralController.instance.UpdateLanguages();

        GameManager.instance.dataSavor.language = UIManager.instance.languageManager.GetLanguage();
        GameManager.instance.SaveData();
    }


    //设置音乐音量
    public void SetMusicVolume()
    {
        mixer.SetFloat("Music", musicSlider.value);
        if (!GameManager.instance) return;
        GameManager.instance.dataSavor.music = musicSlider.value;
        GameManager.instance.SaveData();
    }
    //设置音效音量
    public void SetEffectVolume()
    {
        mixer.SetFloat("Effect", effectSlider.value);
        if (!GameManager.instance) return;
        GameManager.instance.dataSavor.sfx = effectSlider.value;
        GameManager.instance.SaveData();
    }



    public void BackButton()
    {
        Close();
        mainPanel.Open();
    }

}

 这里呢我们再看流程的控制问题,比如再改状态下我们回到主panel的方式是直接调用mainPanel.Open()。并且为了使得按下esc按键也可以返回,添加了一个Update函数,但是有时候我们很不情愿在一个UI功能下使用Update函数,因此我们可以将Update需要的过程都放在场景控制类下,然后每个UI界面对应场景控制类下state不同的值,这样监测按钮的过程都由场景控制类来进行了。比如我们关闭控制游戏设置界面的方式为界面向上移出界面,主UI界面向上移入界面,在此过程中为了使得场景控制类不能做任何操作,可以将类场景控制类的状态为0状态,即不能做任何操作,主界面完全移入后再通过结束事件设置场景控制类的状态为主界面对应的状态值。

 

posted @ 2019-04-22 23:27  小辉歌  阅读(2224)  评论(0编辑  收藏  举报