内容纲要

游戏简介

  • 此游戏是照着4399上的《圆球撞高塔》做的一款小游戏,游戏的内容就是一个小坦克打圆环


游戏的框架

  • 此游戏是用MVC框架来编写的
  • 此MVC框架可参考金老师的mvc_v2视频
  • 在Assets文件夹下创建Scripts文件夹,再在Scripts文件夹下创建Framework和Appication文件夹
  • 在Appication文件夹下创建Model、View、Controller文件夹
  • 把MVC、ModelBase、ViewBase、ControllerBase脚本放到Framework文件夹下

MVC、ModelBase、ViewBase、ControllerBase脚本的代码

MVC脚本的代码
/// <summary>
/// 此类作为MVC框架对外的接口,此类为单件类,主要负责:注册和存储MVC实例、获取MVC实例、发送事件
/// </summary>
public class MVC
{
    public static MVC instance = new MVC(); //静态成员 MVC实例

    private MVC() { } //私有构造器,外部不能构造MVC的实例,所以此类只有instance这一个实例(单件模式)

    //用字典存储MVC
    public Dictionary<string, ViewBase> views = new Dictionary<string, ViewBase>(); //视图字典
    public Dictionary<string, ModelBase> models = new Dictionary<string, ModelBase>(); //模型
    public Dictionary<string, Type> ctrls = new Dictionary<string, Type>(); //控制

    /// <summary>
    /// 注册模型
    /// 使用范例
    /// bm = new BagModel();
    /// MVC.instance.RegisterModel(bm);
    /// </summary>
    /// <param name="model"></param>
    public void RegisterModel(ModelBase model)
    {
        if (models.ContainsKey(model.Name) == true) //如果字典中已经包含了键model.Name(传入的参数model的名字)
        {
            return; //返回,就是此模型已经被注册过了,不需要再注册了
        }
        models[model.Name] = model; //为字典models添加键值,键为ModelBase类型参数model的名字,值为ModelBase类的实例model
    }

    /// <summary>
    /// 注册控制器
    /// 使用范例
    /// MVC.instance.RegisterController(typeof(AttackController)); //调用MVC的注册控制器方法,传入参数控制器类的类型,typeof方法取得对象类型
    /// ctrl = Activator.Create(typeof(AttackController)); //使用Activator.Create方法根据类型构建对象
    /// ctrl.Execute(...); //执行
    /// </summary>
    /// <param name="ctrlType"></param>
    public void RegisterController(string name, Type ctrlType)
    {
        if (ctrls.ContainsKey(name) == true)
        {
            return;
        }
        ctrls[name] = ctrlType;
    }

    //注册视图
    public void RegisterView(ViewBase view)
    {
        if (views.ContainsKey(view.Name) == true)
        {
            return;
        }
        views[view.Name] = view;

        view.interestedEvents = view.GetInterestedEvents(); //获取ViewBase派生类对象关心的事件,保存到它的成员关心的事件数组中
    }

    //发送事件(控制器先响应,视图再响应)
    //事件对应的处理者有两种:控制器(View → Controller) ; View(Model → View)
    public void SendEvent(string eventName, object eventParam) //传入事件名与事件参数
    {
        if (ctrls.ContainsKey(eventName)) //如果控制器字典中有传入的参数事件名字这个键
        {
            Type ctrlType = ctrls[eventName]; //获取控制器字典键事件名字对应的值
            var cb = Activator.CreateInstance(ctrlType) as ControllerBase; //根据类型创建对象,对象的object类型的,用as转换成ControllerBase类型
            cb.Execute(eventParam); //执行
            return;
        }

        foreach (var view in views) //遍历视图字典
        {
            if (view.Value.interestedEvents.Contains(eventName)) //如果view.Value(字典的值,视图类)的关心的事件数组中包含eventName(传入的参数事件名称)
            {
                view.Value.HandleEvents(eventName, eventParam); //处理事件
            }
        }
    }
}
ModelBase脚本的代码
/// <summary>
/// 数据模型,每个模型必须有一个名字
/// </summary>
public abstract class ModelBase //数据模型抽象类
{
    public abstract string Name { get; } //属性 名字
}
ViewBase脚本的代码
//视图类
//1.每个从ViewBase继承的类都必须提供一个名字
//2.每个从ViewBase继承的类都必须注册自己关心的事件
//3.每个从ViewBase继承的类都需要处理自己关心的事件
public abstract class ViewBase : MonoBehaviour
{
    public IList<string> interestedEvents; //关心的事件列表

    public abstract string Name { get; } //Name属性,有一个get访问器

    public abstract IList<string> GetInterestedEvents(); //获得感兴趣的事件

    public abstract void HandleEvents(string eventName, object eventParam); //处理事件
}
ControllerBase脚本的代码
/// <summary>
/// 执行命令(一段功能代码)
/// </summary>
public abstract class ControllerBase {
    public abstract void Execute(object param); //执行
}

管理器的创建

  • 在层级视图中创建一个根节点,重命名为MainSceneManager
  • 在Appication文件夹下创建一个脚本,重命名为MainSceneManager,将MainSceneManager脚本挂到MainSceneManager节点上
  • MainSceneManager脚本唯一功能就是在Start方法中注册Model、View和Controller

游戏场景数据的编写

LevelDataModel关卡数据

  • 在Model文件夹下创建脚本LevelDataModel

  • 此类是存放单个关卡数据的

  • LevelDataModel类继承ModelBase

  • 此类需要加上可序列化标记[Serializable]

  • 定义成员变量

public int id; //id号
public bool isUnlock; //是否解锁

public string tank; //坦克
public string cannonball; //炮弹

public string ground; //地面
public int groundType; //地面型号
public int groundPitchNumber; //地面节数

public int ringObstacleQuantity; //圆环障碍物数量
public float rotationalSpeed; //圆环障碍物旋转速度
public int timeToTurn; //转向时间(多久改变一次旋转的方向)
public List<string> ringObstacle; //圆环障碍物
public List<int> ringObstacleDifferentialAngle; //圆环障碍物角度差

public int circularRingQuantity; //圆环数量
public List<string> circularRing; //圆环
public int circularRingDifferentialAngle; //圆环角度差(两个相邻圆环的角度差)

public List<string> treasure; //财宝(钻石、宝箱)
public List<int> treasureHp; //财宝生命值

public int circularRingQuantityAddtreasureHp; //圆环数加财宝生命值
  • 实现ModelBase的抽象属性
public override string Name
{
    get
    {
        return ("LevelDataModel"+id);
    }
}

LevelDatasModel关卡数据集合

  • 在Model文件夹下创建脚本LevelDatasModel
  • 此类是存放全部关卡数据的
  • LevelDatasModel类继承ModelBase
  • 此类需要加上可序列化标记[Serializable]
  • 实现基类抽象属性
public override string Name
{
    get
    {
        return "LevelDatasModel";
    }
}
  • 创建成员变量与方法
//动态数组lDMS,此数组用来存放LevelDataModel
public List<LevelDataModel> lDMS = new List<LevelDataModel>();

//往lDMS数组中添加元素的方法
public void AddElement(LevelDataModel lDM)
{
    lDMS.Add(lDM);
}

//往lDMS数组中删除元素的方法
public void RemoveElement(LevelDataModel lDM)
{
    lDMS.Remove(lDM);
}
  • 在MainSceneManager脚本中注册LevelDatasModel,写在Start方法中
MVC.instance.RegisterModel(new LevelDatasModel()); //注册关卡数据集合模型

UI的搭建

创建场景

  • 在Assets下创建文件夹Scenes,在Scenes文件夹下创建一个Scene,并重命名为MainScene
  • 打开MainScene,然后把Game窗口的分辨率设置为9:16(竖屏窗口)

在MainScene场景中搭建UI

  • 在层级视图中创建一个Canvas

  • 在Canvas下创建用一个Panel,并重命名为MainPanel,为MainPanel节点添加两个AudioSource组件

  • 在MainPanel下创建一个空节点,命名为DataStore,在DataStore下创建一个Button和一个Text,并将Button重命名DataStoreButton,之后在DataStoreButton的子节点Text中写上“将关卡数据存为json文件”,参数设置参考图片


  • 再在MainPanel下创建一个空节点,命名为InitialInterface,在InitialInterface下创建一个Button和一个Text,并将Button重命名PlayButton,Text重命名为GameNameText,PlayButton的子节点Text重命名为PlayText,之后在GameNameText中写上“Fire Balls 3D”,在PlayText中写上“Play”,参数设置参考图片

  • 在Assets下创建一个文件夹Textures,在此文件夹下放入一些图片资源,将所有图片的TextureType改为Sprite(2D and UI),这些图片是我在4399的圆球撞高塔游戏中用截图截的,然后用ps修改了一下,也可以在网上找类似的资源

  • 再在MainPanel下创建一个空节点,命名为CheckpointChoice,在CheckpointChoice下创建一个Button和一个Scroll View,并将Button重命名ReturnButton,再将ReturnButton的Image组件的Source Image成员设置为返回纹理,然后为Scroll View的孙节点Content添加组件Grid Layout Group和Content Size Fitter,参数设置参考图片

  • 再在MainPanel下创建一个空节点,命名为LevelFail,在LevelFail下创建两个Button和一个Text,并将两个Button分别重命名为ReplayButton和ReturnInitialInterfaceButton,再将ReplayButton的Image组件的Source Image成员设置为重新开始纹理,ReturnInitialInterfaceButton的Image组件的Source Image成员设置为返回初始界面纹理,在Text中写上“Level Fail”,参数设置参考图片

  • 再在MainPanel下创建一个空节点,命名为LevelCleared,在LevelCleared下创建两个Button和一个Text,并将两个Button分别重命名为NextLevelButton和ReturnInitialInterfaceButton,再将NextLevelButton的Image组件的Source Image成员设置为进入下一关纹理,ReturnInitialInterfaceButton的Image组件的Source Image成员设置为返回初始界面纹理,在Text中写上“Level CLEARED”,参数设置参考图片

  • 再在MainPanel下创建一个空节点,命名为CustomsClearance,在CustomsClearance下创建一个Button和一个Text,并将Button重命名为ReturnInitialInterfaceButton,再将ReturnInitialInterfaceButton的Image组件的Source Image成员设置为返回初始界面纹理,在Text中写上“恭喜你通关了”,参数设置参考图片

实现游戏初始功能(就是游戏启动时就需要执行的功能)

测试场景的创建

  • 在Scenes文件夹中创建一个Scene,重命名为TestScene
  • TestScene是一个测试场景,游戏运行时是不需要这个场景的
  • 打开TestScene场景,接下来的预制体创建就在这个场景中完成

加载关卡按钮的制作

  • 创建一个Button,重命名为LevelButton
  • 创建一个LevelButtonView脚本,挂到LevelButton上
  • LevelButtonView脚本的编写
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class LevelButtonView : ViewBase //继承ViewBase
{
    public int id; //按钮id

    public Sprite locked; //图片精灵 锁上的 //是从Project拉入进来的纹理贴图
    public Sprite unlock; //图片精灵 已解锁

    public override string Name //重写属性名字
    {
        get
        {
            return ("LevelButtonView" + id);
        }
    }

    public void OnButtonClick() //当按钮被点击
    {
        //发送加载关卡事件
        MVC.instance.SendEvent("LoadLevelEvent", id);
    }

    void Start()
    {
        //为当前按钮添加监听者为OnButtonClick()
        GetComponent<Button>().onClick.AddListener(OnButtonClick);
    }

    //获取关心的事件
    public override IList<string> GetInterestedEvents()
    {
        //关心的事件为:关卡解锁事件
        return new string[] { "LevelUnlockEvent" };
    }

    //处理事件
    public override void HandleEvents(string eventName, object eventParam)
    {
        switch (eventName) //以事件名为开关
        {
            case "LevelUnlockEvent": //如果是 关卡解锁事件
                {
                    //如果被解锁的关卡数据的id等于当前按钮id
                    if ((int)eventParam == id)
                    {
                        //将按钮的sprite换成已解锁sprite
                        GetComponent<Image>().sprite = unlock;

                        //按钮的子节点Text中显示 按钮id
                        transform.Find("Text").gameObject.GetComponent<Text>().text = id.ToString();
                    }
                    break;
                }
        }
    }
}
  • 在Assets文件夹下创建Resources文件夹,然后在Resources文件下创建Prefabs文件夹,再在Prefabs文件夹下创建UI文件夹,将LevelButton拉到Resources/Prefabs/UI 文件夹中,然后把层级视图中的LevelButton删除,这样预制体就创建好了

MainPanelView脚本的编写

  • 创建一个MainPanelView脚本
  • 将此脚本挂到MainPanel节点上
  • 在MainSceneManager脚本中注册MainPanelView
MVC.instance.RegisterView(GameObject.Find("MainPanel").GetComponent<MainPanelView>()); //注册主面板视图
  • MainPanelView类继承ViewBase
  • 添加成员变量
public Transform initialInterface; //初始界面
public Transform checkpointChoice; //关卡选择界面
public Transform levelFail; //失败界面
public Transform levelCleared; //胜利界面
public Transform customsClearance; //通关界面

public AudioSource as1; //AudioSource组件
public AudioSource as2; //AudioSource组件
public AudioClip ac; //音频
public AudioClip bgm; //音频

private int currentLevelId; //当前关卡id

bool isReadingData = false; //关卡数据已经读取过了吗

public GameObject levelButton; //加载关卡按钮(预制体)
  • 在Start中为成员变量赋值,并为各种按钮添加监听器(有些事件暂时没有处理程序,后期会处理)
void Start()
{
    //获取当前节点(MainPanel)的子节点,并赋给成员变量
    initialInterface = transform.Find("InitialInterface");
    checkpointChoice = transform.Find("CheckpointChoice");
    levelFail = transform.Find("LevelFail");
    levelCleared = transform.Find("LevelCleared");
    customsClearance = transform.Find("CustomsClearance");

    //获取当前节点(MainPanel)上的所有AudioSource类型的组件
    var as_array = gameObject.GetComponents(typeof(AudioSource));
    //将AudioSource组件赋给成员变量
    as1 = (AudioSource)as_array[0];
    as2 = (AudioSource)as_array[1];

    //为初始界面上的PlayButton添加监听器
    initialInterface.Find("PlayButton").gameObject.GetComponent<Button>().onClick.AddListener(() =>
    {
        //发送切换面板事件
        MVC.instance.SendEvent("SwitchPanelEvent", new object[] { checkpointChoice.gameObject, true, initialInterface.gameObject, false });
        //发送音频事件
        MVC.instance.SendEvent("AudioEvent", new object[] { "play", false, ac, as2 });
    });

    //为关卡选择界面的ReturnButton添加监听器
    checkpointChoice.Find("ReturnButton").gameObject.GetComponent<Button>().onClick.AddListener(() =>
    {
        MVC.instance.SendEvent("SwitchPanelEvent", new object[] { initialInterface.gameObject, true, checkpointChoice.gameObject, false });
        MVC.instance.SendEvent("AudioEvent", new object[] { "play", false, ac, as2 });
    });

    //为失败界面的ReplayButton添加监听器
    levelFail.Find("ReplayButton").gameObject.GetComponent<Button>().onClick.AddListener(() =>
    {
        MVC.instance.SendEvent("AudioEvent", new object[] { "play", false, ac, as2 });
        //发送销毁事件
        MVC.instance.SendEvent("DestroyEvent", null);
        //发送加载关卡事件
        MVC.instance.SendEvent("LoadLevelEvent", currentLevelId);
    });

    //为失败界面的ReturnInitialInterfaceButton添加监听器
    levelFail.Find("ReturnInitialInterfaceButton").gameObject.GetComponent<Button>().onClick.AddListener(() =>
    {
        MVC.instance.SendEvent("DestroyEvent", null);
        MVC.instance.SendEvent("SwitchPanelEvent", new object[] { levelFail.gameObject, false, initialInterface.gameObject, true });
        MVC.instance.SendEvent("AudioEvent", new object[] { "play", false, ac, as2 });
    });

    //为胜利界面的NextLevelButton添加监听器
    levelCleared.Find("NextLevelButton").gameObject.GetComponent<Button>().onClick.AddListener(() =>
    {
        currentLevelId++; //当前关卡id号加1,
        MVC.instance.SendEvent("DestroyEvent", null);
        MVC.instance.SendEvent("SwitchPanelEvent", new object[] { levelCleared.gameObject, false });
        MVC.instance.SendEvent("AudioEvent", new object[] { "play", false, ac, as2 });
        MVC.instance.SendEvent("LoadLevelEvent", currentLevelId);
    });

    //为胜利界面的ReturnInitialInterfaceButton添加监听器
    levelCleared.Find("ReturnInitialInterfaceButton").gameObject.GetComponent<Button>().onClick.AddListener(() =>
    {
        MVC.instance.SendEvent("DestroyEvent", null);
        MVC.instance.SendEvent("SwitchPanelEvent", new object[] { levelCleared.gameObject, false, initialInterface.gameObject, true });
        MVC.instance.SendEvent("AudioEvent", new object[] { "play", false, ac, as2 });
    });

    //为通关界面的ReturnInitialInterfaceButton添加监听器
    customsClearance.Find("ReturnInitialInterfaceButton").gameObject.GetComponent<Button>().onClick.AddListener(() =>
    {
        MVC.instance.SendEvent("DestroyEvent", null);
        MVC.instance.SendEvent("SwitchPanelEvent", new object[] { customsClearance.gameObject, false, initialInterface.gameObject, true });
        MVC.instance.SendEvent("AudioEvent", new object[] { "play", false, ac, as2 });
    });
}
  • 重写基类的抽象方法(发送的一些事件暂时没有处理程序,后期会处理)
public override string Name
{
    get
    {
        return "MainPanelView";
    }
}

//获取关心的事件
public override IList<string> GetInterestedEvents()
{
    //关心的事件有:读盘成功事件、关卡加载成功事件、游戏结束事件
    return new string[] { "ReadSuccessfullyEvent", "LevelLoadSuccessEvent", "GameOverEvent" };
}

//处理事件
public override void HandleEvents(string eventName, object eventParam)
{
    switch (eventName) //事件名为开关
    {
        case "ReadSuccessfullyEvent": //如果是读盘成功事件
            {
                //将读取的json文件反序列化生成的LevelDatasModel赋给models字典中的LevelDatasModel
                MVC.instance.models["LevelDatasModel"] = eventParam as LevelDatasModel;
                break;
            }
        case "LevelLoadSuccessEvent": //如果是关卡加载成功事件
            {
                currentLevelId = (int)eventParam; //为当前关卡id赋值
                //发送切换面板事件(把关卡选择界面和失败界面关闭)
                MVC.instance.SendEvent("SwitchPanelEvent", new object[] { checkpointChoice.gameObject, false, levelFail.gameObject, false });
                //发送音频事件(用as2播放音频ac)
                MVC.instance.SendEvent("AudioEvent", new object[] { "play", false, ac, as2 });
                //将当前面板(MainPanel)隐藏(透明化)
                GetComponent<Image>().color = new Color(0, 0, 0, 0);
                break;
            }
        case "GameOverEvent": //处理游戏结束事件
            {
                string s = (string)eventParam;
                if (s == "lose") //如果输
                {
                    //发送切换面板事件(开启失败界面)
                    MVC.instance.SendEvent("SwitchPanelEvent", new object[] { levelFail.gameObject, true });
                }
                else if (s == "win") //如果赢
                {
                    //取的字典models中的LevelDatasModel
                    LevelDatasModel lDsM = (LevelDatasModel)MVC.instance.models["LevelDatasModel"];
                    //如果当前关卡id小于总关卡数(关卡id从1计数,关卡数量也是从1计数,所以关卡id小于总关卡数就说明当前关卡不是最后一关)
                    if (currentLevelId < lDsM.lDMS.Count)
                    {
                        //发送切换面板事件(打开胜利界面)
                        MVC.instance.SendEvent("SwitchPanelEvent", new object[] { levelCleared.gameObject, true });
                    }
                    else //否则(就是当前关卡是最后一关)
                    {
                        //发送切换面板事件(打开通关界面)
                        MVC.instance.SendEvent("SwitchPanelEvent", new object[] { customsClearance.gameObject, true });
                    }

                    //遍历所以关卡数据数组
                    foreach (LevelDataModel ldm in lDsM.lDMS)
                    {
                        //如果此关卡数据的id等于当前关卡id+1
                        if (ldm.id == currentLevelId + 1)
                        {
                            //解锁此关卡(就是胜利后解锁下一关)
                            ldm.isUnlock = true;
                        }
                    }

                    //发送数据事件(将关卡数据合集json化,为了保存关卡解锁信息,相当于存档)
                    MVC.instance.SendEvent("DataEvent", new object[] { "save", "JsonFiles", lDsM.Name, lDsM, null });
                }

                //将当前对象(MainPanel)显示出来(取消透明化)
                GetComponent<Image>().color = new Color(0, 0, 0, 0.7f);
                break;
            }
    }
}
  • 在Update中读取关卡数据(暂时读不出来数据,因为还没创建具体的关卡数据,就是没有json文件,并且还没有创建读写json文件的脚本,之后会处理)
private void Update()
{
    //如果关卡数据未读取
    if (isReadingData == false)
    {
        //发送音频事件(用as1播放bgm,并且循环播放)
        MVC.instance.SendEvent("AudioEvent", new object[] { "play", true, bgm, as1 });

        //发送数据事件(反序列化json文件)
        MVC.instance.SendEvent("DataEvent", new object[] { "load", "JsonFiles", "LevelDatasModel", null, typeof(LevelDatasModel) });

        //取的models字典中的LevelDatasModel(因为上面发送过数据事件,所以字典中的LevelDatasModel就是json文件反序列化生成的LevelDatasModel)
        LevelDatasModel lDsM = MVC.instance.models["LevelDatasModel"] as LevelDatasModel;

        //遍历关卡数据数组
        for (int i = 0; i < lDsM.lDMS.Count; i++)
        {
            //在Content文件夹下生成关卡按钮(因为是遍历关卡数据数组,所以最后生成的按钮的数量就是关卡数据的数量,就是有几关就生成几个按钮)
            GameObject lB = Instantiate(levelButton, checkpointChoice.Find("Scroll View/Viewport/Content"));
            //获取到生成的关卡按钮的LevelButtonView组件
            LevelButtonView lBV = lB.GetComponent<LevelButtonView>();
            //按钮的id为i+1,就是第1个按钮的id是0+1,第2个按钮的id是1+1,等等等等
            lBV.id = i + 1;
            //注册LevelButtonView
            MVC.instance.RegisterView(lBV);
        }
        //遍历关卡数据数组
        for (int i = 0; i < lDsM.lDMS.Count; i++)
        {
            //如果此关卡数据是解锁的(就是此关是解锁的)
            if (lDsM.lDMS[i].isUnlock)
            {
                //发送关卡解锁事件,LevelButtonView会处理(根据关卡数据的id,解锁相应id的按钮)
                MVC.instance.SendEvent("LevelUnlockEvent", lDsM.lDMS[i].id);
            }
        }

        //关卡数据已读取
        isReadingData = true;
    }
}

数据读写(创建关卡数据并json化)

创建专门用来读写数据的脚本

  • 在Controller文件夹下创建一个脚本,重命名为DataController
  • 在MainSceneManager脚本中注册DataController
MVC.instance.RegisterController("DataEvent", typeof(DataController)); //注册数据控制器
  • DataController继承ControllerBase
  • 重写基类的抽象方法
/// <summary>
/// 执行DataEvent的方法
/// </summary>
/// <param name="param">
/// param是一个object[]
/// param[0]是一个string,表示要进行的操作,save(储存,将对象json化写盘),load(加载,读取json文件构造对象),cover(覆盖,读取json文件覆盖对象数据)
/// param[1]是一个string,为json文件要放入的文件夹名称
/// param[2]是一个string,为json文件的文件名
/// param[3]是一个object,为要被json化的对象
/// param[4]是一个Type,json对象的类型
/// </param>
public override void Execute(object param)
{
    string saveOrLoadOrCover = ((object[])param)[0] as string;
    string folderName = ((object[])param)[1] as string;
    string fileName = ((object[])param)[2] as string;
    object obj = ((object[])param)[3];
    Type type = ((object[])param)[4] as Type;

    if (saveOrLoadOrCover == "save")
    {
        //将对象序列化为json字符串
        string jsonString = JsonUtility.ToJson(obj, true);
        //将json字符串写盘(写到指定文件夹下的指定文件中,如果指定文件夹下没有指定的文件就会生成指定名字的文件,然后写入)
        File.WriteAllText(Application.streamingAssetsPath + ("/" + folderName + "/") + (fileName + ".json"), jsonString);

        //发送写盘成功事件
        MVC.instance.SendEvent("WriteSuccessfullyEvent", obj);
    }
    else if (saveOrLoadOrCover == "load")
    {
        string jsonString = null; //创建一个字符串
        try //试跑
        {
            //读取指定文件夹下的指定json文件(如果没有找到指定json文件就会抛异常)
            jsonString = File.ReadAllText(Application.streamingAssetsPath + ("/" + folderName + "/") + (fileName + ".json"));
        }
        catch (FileNotFoundException e) //捕获异常
        {
            //发送一个读盘失败事件
            MVC.instance.SendEvent("ReadFailedEvents", e);
        }

        //反序列化json字符串,生成对象
        object ob = JsonUtility.FromJson(jsonString, type);

        if (ob != null) //如果对象不为空
        {
            //发送读盘成功事件
            MVC.instance.SendEvent("ReadSuccessfullyEvent", ob);
        }

    }
    else if (saveOrLoadOrCover == "structure")
    {
        //暂时没有功能
    }
}

实现便捷构建关卡数据并写盘功能

  • 在View文件夹下新建一个脚本,重命名为DataStoreView
  • 将DataStoreView挂到DataStore节点上
  • DataStoreView类继承ViewBase
  • 定义成员变量,跟LevelDataModel类的成员变量一样
public int id; //id号
public bool isUnlock; //是否解锁

public string tank; //坦克
public string cannonball; //炮弹

public string ground; //地面
public int groundType; //地面型号

public int ringObstacleQuantity; //圆环障碍物数量
public float rotationalSpeed; //圆环障碍物旋转速度
public int timeToTurn; //转向时间(多久改变一次旋转的方向)
public List<string> ringObstacle; //圆环障碍物
public List<int> ringObstacleDifferentialAngle; //圆环障碍物角度差

public int circularRingQuantity; //圆环数量
public List<string> circularRing; //圆环
public int circularRingDifferentialAngle; //圆环角度差(两个相邻圆环的角度差)

public List<string> treasure; //财宝(钻石、宝箱)
public List<int> treasureHp; //财宝生命值
  • 创建一个用来存关卡数据的方法方法
//方法 当数据存储按钮被点击
public void OnDataStoreButtonClick()
{
    //创建一个LevelDataModel(关卡数据)类型的局部变量lDM
    LevelDataModel lDM = new LevelDataModel();

    //为lDM赋值
    lDM.id = id;
    lDM.isUnlock = isUnlock;
    lDM.tank = tank;
    lDM.cannonball = cannonball;
    lDM.ground = ground;
    lDM.groundType = groundType;
    lDM.groundPitchNumber = groundType + 1; //地面节数为地面型号加1(因为地面节数最少就是两节,而型号从1开始,所以两节地面的型号为1,三节地面的型号为2 .....)
    lDM.ringObstacleQuantity = ringObstacleQuantity;
    lDM.rotationalSpeed = rotationalSpeed;
    lDM.timeToTurn = timeToTurn;
    lDM.ringObstacle = ringObstacle;
    lDM.ringObstacleDifferentialAngle = ringObstacleDifferentialAngle;
    lDM.circularRingQuantity = circularRingQuantity;
    lDM.circularRing = circularRing;
    lDM.circularRingDifferentialAngle = circularRingDifferentialAngle;
    lDM.treasure = treasure;
    lDM.treasureHp = treasureHp;

    //创建一个LevelDatasModel(关卡数据集合)类型局部变量IDsM
    LevelDatasModel lDsM;
    //取得models字典中的LevelDatasModel(关卡数据集合),赋给局部变量IDsM,因为程序启动时会在MainPanelView脚本中为models字典中的LevelDatasModel赋值,所以字典中的关卡数据集合就是json文件中的关卡数据集合
    lDsM = MVC.instance.models["LevelDatasModel"] as LevelDatasModel;

    //遍历关卡数据集合中的关卡数组(数组的元素是关卡数据)
    for (int i = 0; i < lDsM.lDMS.Count; i++)
    {
        //如果此关卡数据的id等于lDM的id(lDM是上面创建的局部变量)
        if (lDsM.lDMS[i].id == lDM.id)
        {
            //删除此关卡数据(除旧,防止出现多个关卡数据id相同,就是防止同一关拥有多个关卡数据)
            lDsM.RemoveElement(lDsM.lDMS[i]);
        }
    }
    lDsM.AddElement(lDM); //把lDM添加到lDsM数组中(就是把新建的关卡数据加入到关卡数据集合中)

    //发送数据事件(把关卡数据集合json化写盘)
    MVC.instance.SendEvent("DataEvent", new object[] { "save", "JsonFiles", lDsM.Name, lDsM, null });
}
  • 在Start中为DataStore节点下的DataStoreButton添加监听器
transform.Find("DataStoreButton").gameObject.GetComponent<Button>().onClick.AddListener(OnDataStoreButtonClick);
  • 重写抽象基类的虚方法
public override string Name
{
    get
    {
        return "DataStoreView";
    }
}

//获取关心的事件
public override IList<string> GetInterestedEvents()
{
    //关心的事件有:写盘成功事件
    return new string[] { "WriteSuccessfullyEvent" };
}

//处理事件
public override void HandleEvents(string eventName, object eventParam)
{
    switch (eventName) //以事件名为开关
    {
        case "WriteSuccessfullyEvent": //如果是写盘成功事件
            {
                //获取字典中的关卡数据集合
                LevelDatasModel lDsM = MVC.instance.models["LevelDatasModel"] as LevelDatasModel;

                //在当前节点(DataStore)的子节点Text中输出写盘成功的提示
                transform.Find("Text").gameObject.GetComponent<Text>().text = string.Format("第{0}关数据Json化写盘成功", lDsM.lDMS[lDsM.lDMS.Count - 1].id);
                break;
            }
    }
}
  • 在MainSceneManager脚本中注册DataStoreView
MVC.instance.RegisterView(GameObject.Find("MainPanel").transform.Find("DataStore").gameObject.GetComponent<DataStoreView>()); //注册数据存储视图
  • 下面是存关卡数据教程(暂时还不能存,因为场景中的很多东西都是预制体,预制体还没做)

  • 把MainPanel的子节点大部分禁用只开启DataStore

  • 在DataStore节点的Inspector中把数据填好

  • 然后启动程序,点击DataStoreButton,第一关的关卡数据就存好了

创建游戏场景中的各种预制体(所以关卡都是根据预制体生成的)

导入插件

  • 在AssetStore中找到ProBuilder插件

  • 导入ProBuilder插件

  • 打开ProBuilder的编辑器窗口

  • 打开测试场景TestScene,接下来的预制体在测试场景中创建

地面的创建

  • 创建一个空节点,重命名为MainGround1,就是1号地面,Reset一下

  • 在MainGround1下创建一个空节点SecondaryGround1,就是第一节地段,Reset一下

  • 用ProBuilder创建一个圆盘(如下图)

  • 创建一个空节点,重命名为Disc,将圆盘拉入Disc,然后设置一下参数(如下图)

  • 再在Disc下创建两个节点,分别重命名为RotationCenter1和RotationCenter2,然后Reset一下

  • 创就一个Cube

  • 创建一个空节点,重命名为Road,Reset一下,然后将Cube拉入Road,设置一下参数(如下图)

  • 创建一个3DText,重命名为Text

  • 将Text拉入Road,设置一下参数

  • 将Road拉入SecondaryGround1,然后设置一下参数(如下图)

  • 将SecondaryGround1拷贝一份,重命名为SecondaryGround2

  • 让后设置一下SecondaryGround1的Transform参数(如图)

  • 在Assets/Resources/Prefabs下创建一个文件夹,命名为Grounds

  • 将MainGround1拉入Grounds,这样1号地面就创建好了

坦克的创建

  • 创建一个Cube,重命名为Cube1

  • 创建一个空节点,重命名为Tank1,将Cube1拉入Tank1,设置一下参数(如图)

  • 再创建一个Cube,重命名为Cube2

  • 将Cube2拉入Tank1,设置一下参数(如图)

  • 创建一个空节点,重命名为GunMuzzle,拉入Tank1,设置一下参数(如图)

  • 然后把坦克刷黑,也可为其创建一个黑色的材质

  • 在Assets/Resources/Prefabs文件夹下创建一个新文件夹,重命名为Tanks

  • 将Tank1拉入Tanks,这样1号坦克就创建好了

障碍物的创建

  • 创建一个Arch

  • 创建一个空节点,重命名为RingObstacle1,就是1号环型障碍物,Reset一下

  • 将Arch拉入RingObstacle1,设置一下参数(如图)

  • 然后把障碍物刷蓝,也可为其创建一个蓝色的材质

  • 在Assets/Resources/Prefabs文件夹下创建一个新文件夹,重命名为RingObstacles

  • 将RingObstacle1拉入RingObstacles文件夹,这样1号障碍物就创建好了

  • 再按创建1号障碍物的方法创建2号、3号障碍物

圆环的创建

  • 创建一个Pipe

  • 把Pipe刷红

  • 创建一个空节点,重命名为CircularRing1,Reset一下

  • 将Pipe拉到CircularRing1下,让CircularRing1在圆环的中心底部

  • 在Assets/Resources/Prefabs文件夹下创建一个新文件夹,重命名为CircularRings

  • 将CircularRing1拉入CircularRings,这样1号圆环就创建好了(也不一定要圆环,可以是其它形状,第一关是圆环)

  • 按照创建CircularRing1的方式创建一个CircularRing2(黄色的)

财宝的创建

Treasure1(财宝1,就是钻石)
  • 创建一个Cone

  • 把Cone刷蓝

  • 创建一个空节点,重命名为Treasure1,Reset一下

  • 将Cone拉到Treasure1下,让Treasure1在钻石的底部中心

  • 在Assets/Resources/Prefabs文件夹下创建一个新文件夹,重命名为Treasures

  • 将Treasure1拉入Treasures,这样财宝1就创建好了

Treasure2(财宝2,就是宝箱)
  • 创建一个Cube

  • 把Cube刷黄

  • 创建一个空节点,重命名为Treasure2,Reset一下

  • 将Cube拉到Treasure2下,让Treasure2在宝箱的底部中心,并设置一些参数(如图)

  • 为Treasure2做两个动画(一个放大缩小,一个螺旋升天)

  • 将Treasure2拉入Treasures,这样财宝2就创建好了

炮弹的创建

  • 创建一个Sphere

  • 将Sphere重命名为Cannonball1,然后设置一些参数,并为其创建一个黑色的材质

    • 在Assets/Resources/Prefabs文件夹下创建一个新文件夹,重命名为Cannonballs
  • 将Cannonball1拉入Cannonballs,这样炮弹1就创建好了

为一部分事件添加处理程序(Controller)

SwitchPanelEvent的处理者SwitchPanelController

  • 在Assets/Scripts/Appication/Controller文件夹下新建一个脚本,命名为SwitchPanelController
  • SwitchPanelController类继承ControllerBase
  • 重写基类抽象方法
public class SwitchPanelController : ControllerBase //面板开关控制器
{
    /// <summary>
    /// 此方法是SwitchPanelEvent(切换面板事件)的处理程序
    /// </summary>
    /// <param name="param">参数必须是一个object数组,数组中的元素数量(从1开始计数)必须是偶数,所有偶数元素(从0计数,0算作偶数)必须是Panel(GameObject)奇数元素(从0计数)必须是bool(true或false)</param>
    public override void Execute(object param)
    {
        List<GameObject> truePanel = new List<GameObject>(); //被开启的面板
        List<GameObject> falsePanel = new List<GameObject>(); //被关闭的面板

        //遍历object[]中的所有偶数元素 (就是需要关闭(禁用)或开启(启用)的节点(GameObject))
        for (int i = 0; i < ((object[])param).Length; i += 2)
        {
            //根据第i+1个元素(bool值)设置第i个元素(节点GameObject)是否激活(true启用,false禁用)
            (((object[])param)[i] as GameObject).SetActive((bool)((object[])param)[i + 1]);

            //如果第i+1个元素是布尔值true
            if ((bool)((object[])param)[i + 1])
            {
                //将第i个元素(GameObject)加入到被开启的面板数组中
                truePanel.Add(((object[])param)[i] as GameObject);
            }
            else //如果第i+1个元素是布尔值false
            {
                //将第i个元素(GameObject)加入到被关闭的面板数组中
                falsePanel.Add(((object[])param)[i] as GameObject);
            }
        }

        //发送面板已切换事件,传入参数被开启的面板数组和被关闭的面板数组
        MVC.instance.SendEvent("PanelSwitchedEvent", new object[] { truePanel, falsePanel });
    }
}
  • 在MainSceneManager脚本中注册SwitchPanelController
MVC.instance.RegisterController("SwitchPanelEvent", typeof(SwitchPanelController)); //注册面板开关控制器

AudioEvent的处理者AudioController

  • 在Assets/Scripts/Appication/Controller文件夹下新建一个脚本,命名为AudioController
  • AudioController类继承ControllerBase
  • 重写基类抽象方法
/// <summary>
/// 音频控制器
/// param是object[]
/// param[0]是string,如果是"play"就播放音频
/// param[1]是bool,表示音频是否循环
/// param[2]是AudioClip,是要播放的音频
/// param[3]是AudioSource,用此组件进行播放
/// </summary>
public class AudioController : ControllerBase
{
    public override void Execute(object param)
    {
        //如果param(是object数组)的第0个元素是字符串"play"
        if (((object[])param)[0] as string == "play")
        {
            ((((object[])param)[3]) as AudioSource).clip = ((((object[])param)[2]) as AudioClip); //给节点(GameObject)的AudioSource组件的成员clip赋值
            ((((object[])param)[3]) as AudioSource).Play(); //播放音频源
            ((((object[])param)[3]) as AudioSource).loop = ((bool)(((object[])param)[1])); //设置是否循环播放
        }
        else
        {
            ((((object[])param)[3]) as AudioSource).Stop(); //停止音频源
            MVC.instance.SendEvent("StopAudioEvent", null);
        }
    }
}
  • 在MainSceneManager脚本中注册AudioController
MVC.instance.RegisterController("AudioEvent", typeof(AudioController)); //注册音频控制器

生成场景地图

对炮弹预制体进行修改(添加脚本)

  • 在View文件夹下新建一个脚本,命名为CannonballView
  • 编写脚本
public class CannonballView : ViewBase
{
    public override string Name
    {
        get
        {
            return "CannonballView";
        }
    }

    //获取关心的事件
    public override IList<string> GetInterestedEvents()
    {
        //暂时没有关心的事件
        throw new System.NotImplementedException();
    }

    //处理事件
    public override void HandleEvents(string eventName, object eventParam)
    {
        //暂时不需要处理
        throw new System.NotImplementedException();
    }

    // Start is called before the first frame update
    void Start()
    {
        //一秒钟后调用DestroyThisGameObject方法
        Invoke("DestroyThisGameObject", 1);
    }

    //当触发器被触发时
    private void OnTriggerEnter(Collider other)
    {
        //如果与炮弹发生触碰的是圆环
        if (other.tag == "Ring")
        {
            //发送一个销毁圆环事件
            MVC.instance.SendEvent("RingDestroyEvent", other);
            //调用自我销毁方法
            DestroyThisGameObject();
        }
        //如果与炮弹发生触碰的是障碍物
        else if (other.tag == "Obstacle")//如果与炮弹发生触碰的是障碍物
        {
            //发送一个游戏结束事件,参数为字符串lose(输)
            MVC.instance.SendEvent("GameOverEvent", "lose");
            //调用自我销毁方法
            DestroyThisGameObject();
        }
        //如果与炮弹发生触碰的是财宝
        else if (other.tag == "Treasure")
        {
            //发送一个财宝受击事件
            MVC.instance.SendEvent("HitTreasureEvent", other);
            //调用自我销毁方法
            DestroyThisGameObject();
        }
    }

    //自我销毁方法
    private void DestroyThisGameObject()
    {
        Destroy(gameObject);
    }

    // Update is called once per frame
    void Update()
    {
        //炮弹往前飞
        transform.Translate(Vector3.forward * Time.deltaTime * 60);
    }
}
  • 将CannonballView挂到炮弹预制体上,然后保存修改后的预制体

对地面预制体进行修改(添加脚本)

  • 在View文件夹下新建一个脚本,命名为MainGroundView
  • 将MainGroundView挂到地面预制体上,然后保存修改后的预制体
  • 编写MainGroundView脚本
public class MainGroundView : ViewBase
{
    public LevelDataModel lDM; //关卡数据
    public GameObject tank; //坦克
    public GameObject vidicon; //摄像机

    public int LittleLevel; //小关卡(第几小节)
    public int mobileVariable; //移动变量
    //public Transform origin; //起点
    public Transform destination; //目的地

    public GameObject toBeDestroyed; //待销毁物

    public bool isInitializeCircularRing; //是否初始化圆环
    public bool canShoot; //能否射击(圆环生成完毕后才可以射击)
    public bool canDestroy; //能否销毁
    public bool canMove; //能否移动
    public bool alive = true; //是否活着

    public float timer; //计时器
    public float timer2; //计时器2
    public float timer3; //计时器3
    public int counter; //计数器
    public int counter2; //计数器2

    public AudioClip shatter; //破碎(圆环或财宝被攻击时应发出的声音)
    public AudioClip victory; //胜利

    private Animator animator; //动画状态机

    public override string Name
    {
        get
        {
            return "MainGroundView";
        }
    }

    //获取关心的事件
    public override IList<string> GetInterestedEvents()
    {
        //关心的事件有:初始化地图事件、销毁圆环事件、游戏结束事件、销毁关卡事件、财宝受击事件
        return new string[] { "InitializationMapEvent", "RingDestroyEvent", "GameOverEvent", "DestroyLevelEvent", "HitTreasureEvent" };
    }

    //处理事件
    public override void HandleEvents(string eventName, object eventParam)
    {
        //事件名为开关
        switch (eventName)
        {
            //如果是初始化地图事件
            case "InitializationMapEvent":
                {
                    //小关卡加1
                    LittleLevel++;
                    //为移动变量赋值
                    mobileVariable = LittleLevel;

                    //生成圆环障碍物
                    for (int i = (lDM.ringObstacleQuantity / lDM.groundPitchNumber) * (LittleLevel - 1); i < (lDM.ringObstacleQuantity / lDM.groundPitchNumber) * LittleLevel; i++)
                    {
                        //获取关卡数据中的障碍物数组中的第i个元素(圆环障碍物)的名字
                        string ringObstacleName = lDM.ringObstacle[i];
                        //根据名字获取预制体
                        GameObject ringObstacle = Resources.Load("Prefabs/RingObstacles/" + ringObstacleName) as GameObject;
                        //实例化预制体(生成在第LittleLevel节地面的圆盘上的旋转中心2中)
                        GameObject ringObstacleInstance = Instantiate(ringObstacle, transform.Find("SecondaryGround" + LittleLevel + "/Disc/RotationCenter2"));
                        //获取圆环障碍物角度差
                        int angle = lDM.ringObstacleDifferentialAngle[i];
                        //根据圆环障碍物角度差设置一下圆环障碍物的旋转角度
                        ringObstacleInstance.transform.Rotate(0, angle, 0);
                    }

                    //获得圆环与财宝的生命值之和
                    lDM.circularRingQuantityAddtreasureHp = lDM.circularRingQuantity + lDM.treasureHp[LittleLevel - 1];

                    //在第LittleLevel节地面的路段的3DText中显示需攻击的次数(圆环与财宝生命值之和)
                    transform.Find("SecondaryGround" + LittleLevel + "/Road/Text").gameObject.GetComponent<TextMesh>().text = lDM.circularRingQuantityAddtreasureHp.ToString();

                    //是否初始化圆环设为true
                    isInitializeCircularRing = true;

                    break;
                }
            case "RingDestroyEvent": //如果是销毁圆环事件
                {
                    //发送音频事件(音频控制器会播放圆环破碎音效)
                    MVC.instance.SendEvent("AudioEvent", new object[] { "play", false, shatter, gameObject.GetComponent<AudioSource>() });

                    //圆环与财宝的生命值之和减1
                    lDM.circularRingQuantityAddtreasureHp--;

                    //在第LittleLevel节地面的路段的3DText中显示剩余需攻击的次数(减1后的圆环与财宝生命值之和)
                    transform.Find("SecondaryGround" + LittleLevel + "/Road/Text").gameObject.GetComponent<TextMesh>().text = lDM.circularRingQuantityAddtreasureHp.ToString();

                    //获取被攻击的圆环,赋给成员变量待销毁物
                    toBeDestroyed = ((MeshCollider)eventParam).transform.parent.gameObject;

                    //放大待销毁物(被攻击的圆环)
                    toBeDestroyed.transform.localScale = toBeDestroyed.transform.localScale * 1.3f;

                    //能否销毁设为true
                    canDestroy = true;

                    break;
                }
            case "GameOverEvent": //如果是游戏结束事件
                {
                    alive = false; //是否活着设为false(死了)
                    canShoot = false; //能否射击设为false(死了就不能射击了)
                    break;
                }
            case "DestroyLevelEvent": //如果是销毁关卡事件
                {
                    //把摄像机移走(移到Canvas下)
                    vidicon.transform.SetParent(GameObject.Find("Canvas").transform);
                    //自我销毁
                    Destroy(gameObject);
                    break;
                }
            case "HitTreasureEvent": //如果是财宝受击事件
                {
                    //是否活着(一次性发射多个炮弹,当第一发击中障碍物就死了,第二发即使击中财宝也不会处理)
                    if (alive)
                    {
                        //获取第LittleLevel节地面中的圆盘上的旋转中心2
                        Transform RotationCenter2 = transform.Find("SecondaryGround" + LittleLevel + "/Disc/RotationCenter2");
                        //遍历旋转中心2的子节点(子节点是圆环障碍物)
                        foreach (Transform gb in RotationCenter2)
                        {
                            //销毁圆环障碍物
                            Destroy(gb.gameObject);
                        }
                        //发送音频事件(播放破碎音效)
                        MVC.instance.SendEvent("AudioEvent", new object[] { "play", false, shatter, gameObject.GetComponent<AudioSource>() });
                        //圆环与财宝的生命值之和减1
                        lDM.circularRingQuantityAddtreasureHp--;
                        //在第LittleLevel节地面的路段的3DText中显示剩余需攻击的次数(减1后的圆环与财宝生命值之和)
                        transform.Find("SecondaryGround" + LittleLevel + "/Road/Text").gameObject.GetComponent<TextMesh>().text = lDM.circularRingQuantityAddtreasureHp.ToString();

                        //如果小关卡数等于地面节数(就是已经处在最后一节地面上了,只有最后一节地面上的财宝是宝箱)
                        if (LittleLevel == lDM.groundPitchNumber)
                        {
                            //获取被攻击的财宝(宝箱)上的动画状态机
                            animator = ((MeshCollider)eventParam).transform.parent.GetComponent<Animator>();
                            //启用动画状态机
                            animator.enabled = true;
                            //设置动画状态机的布尔值HitBool为true(HitBool为true时宝箱就会播放受击动画UnderAttackAnimation,就是放大缩小)
                            animator.SetBool("HitBool", true);
                        }

                        //如果圆环与财宝的生命值之和小于等于0
                        if (lDM.circularRingQuantityAddtreasureHp <= 0)
                        {
                            //发送音频事件(播放胜利动画)
                            MVC.instance.SendEvent("AudioEvent", new object[] { "play", false, victory, gameObject.GetComponent<AudioSource>() });
                            //在第LittleLevel节地面的路段的3DText中显示空
                            transform.Find("SecondaryGround" + LittleLevel + "/Road/Text").gameObject.GetComponent<TextMesh>().text = "";

                            //如果小关卡数小于地面节数(就是还没到最后一节地面)
                            if (LittleLevel < lDM.groundPitchNumber)
                            {
                                //销毁被攻击的财宝
                                Destroy(((MeshCollider)eventParam).gameObject);

                                //能否移动设为true
                                canMove = true;
                                // 能否射击设为false(往下一小节地面移动的过程中不能射击)
                                canShoot = false;
                                //计时器归零
                                timer = 0;
                                timer2 = 0;
                                timer3 = 0;

                                //获取目的地(当前小节地面的位置(在圆盘处))
                                destination = transform.Find("SecondaryGround" + mobileVariable);
                            }
                            else //否则,就是当前处于最后一小关卡
                            {
                                //设置动画状态机的布尔值WinBool为true(WinBool为true时宝箱就会播放胜利动画VictoryAnimation,就是螺旋升天然后落下)
                                animator.SetBool("WinBool", true);
                            }
                        }
                    }
                    break;
                }
        }
    }

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        //让每一节地面上的圆环与障碍物都转起来
        //遍历地面的地面小节
        for (int i = 1; i <= lDM.groundPitchNumber; i++)
        {
            //获取第i节地面的圆盘上的旋转中心1
            Transform RotationCenter1 = transform.Find("SecondaryGround" + i + "/Disc/RotationCenter1");
            //让旋转中心1旋转
            RotationCenter1.Rotate(Vector3.up, 40 * Time.deltaTime);

            //获取第i节地面的圆盘上的旋转中心2
            Transform RotationCenter2 = transform.Find("SecondaryGround" + i + "/Disc/RotationCenter2");
            //计时器开始计时
            timer += Time.deltaTime;
            //如果计时器时间到达转向时间了,并且转向时间不为0(转向时间为0就代表不需要转向)
            if (timer >= lDM.timeToTurn && lDM.timeToTurn != 0)
            {
                //圆环障碍物的旋转速度取反(就是转向)
                lDM.rotationalSpeed = -lDM.rotationalSpeed;
                //计时器归零
                timer = 0;
            }
            //让旋转中心2按照圆环障碍物的旋转速度来旋转
            RotationCenter2.Rotate(Vector3.up, lDM.rotationalSpeed * Time.deltaTime);
        }

        //生成圆环
        //如果是否初始化圆环为true
        if (isInitializeCircularRing)
        {
            //计时器2开始计时
            timer2 += Time.deltaTime;
            //如果计时器2的时间大于等于0.02
            if (timer2 >= 0.02)
            {
                //遍历第LittleLevel节地面的圆盘中的旋转中心1(旋转中心1的子节点是许多圆环和一个财宝)
                foreach (Transform child in transform.Find("SecondaryGround" + LittleLevel + "/Disc/RotationCenter1"))
                {
                    //将所有子节点的位置往上移0.5f
                    child.position += new Vector3(0, 0.5f, 0);
                }
                timer2 = 0; //计时器2归零
                counter2++; //计数器2加1
            }
            if (counter2 == 2) //如果计数器2的值为2
            {
                int k = counter % lDM.circularRing.Count;
                //获取圆环数组中的第k个元素(圆环名字)
                string circularRingName = lDM.circularRing[k];
                //通过名字找到预制体
                GameObject circularRing = Resources.Load("Prefabs/CircularRings/" + circularRingName) as GameObject;
                //实例化预制体(生成在第LittleLevel节地面的圆盘的旋转中心1中)
                GameObject circularRingInstance = Instantiate(circularRing, transform.Find("SecondaryGround" + LittleLevel + "/Disc/RotationCenter1"));
                //根据圆环角度差设置其旋转度
                circularRingInstance.transform.Rotate(Vector3.up, lDM.circularRingDifferentialAngle * counter);

                counter2 = 0; //计数器2归零
                counter++; //计数器加1

                //如果计时器的值为圆环数量(说明圆环全部生成完毕了)
                if (counter == lDM.circularRingQuantity)
                {
                    //是否初始化圆环设为false(全部生成完毕,不能再生成了)
                    isInitializeCircularRing = false;
                    //能否射击设为true(圆环都生成完毕了,可以射击了)
                    canShoot = true;
                    //计时器归零
                    counter = 0;
                }
            }
        }

        //发射炮弹
        //如果能射击
        if (canShoot)
        {
            //鼠标左键按下
            if (Input.GetMouseButtonDown(0))
            {
                //获取炮弹名字
                string cannonballName = lDM.cannonball;
                //根据名字取得预制体
                GameObject cannonball = Resources.Load("Prefabs/Cannonballs/" + cannonballName) as GameObject;
                //实例化预制体(生成在坦克的枪口下)
                GameObject cannonballInstance = Instantiate(cannonball, tank.transform.Find("GunMuzzle"));
                //重置一下炮弹的位置
                cannonballInstance.transform.localPosition = new Vector3(0, 0, 0);
                //计时器2归零
                timer2 = 0;
            }

            //持续按着鼠标左键
            if (Input.GetMouseButton(0))
            {
                //计时器2开始计时
                timer2 += 10 * Time.deltaTime;
                //如果计时器2的值>=1
                if (timer2 >= 1)
                {
                    //获取炮弹名字
                    string cannonballName = lDM.cannonball;
                    //根据名字取得预制体
                    GameObject cannonball = Resources.Load("Prefabs/Cannonballs/" + cannonballName) as GameObject;
                    //实例化预制体(生成在坦克的枪口下)
                    GameObject cannonballInstance = Instantiate(cannonball, tank.transform.Find("GunMuzzle"));
                    //重置一下炮弹的位置
                    cannonballInstance.transform.localPosition = new Vector3(0, 0, 0);
                    //计时器2归零
                    timer2 = 0;
                }
            }
        }

        //销毁被击中后放大的圆环
        //如果能销毁
        if (canDestroy)
        {
            //计时器3开始计时
            timer3 += Time.deltaTime;

            if (timer3 >= 0.02)
            {
                //遍历第LittleLevel节地面的圆盘的旋转中心1的所有子节点(圆环和财宝)
                foreach (Transform child in transform.Find("SecondaryGround" + LittleLevel + "/Disc/RotationCenter1"))
                {
                    //如果不是待销毁的圆环
                    if (child.gameObject != toBeDestroyed)
                    {
                        //往下落一点
                        child.position -= new Vector3(0, 0.5f, 0);
                    }
                    else //否则,就是待销毁的圆环
                    {
                        //待销毁的圆环上移一点
                        toBeDestroyed.transform.localPosition += new Vector3(0, 0.5f, 0);
                    }
                }
                timer3 = 0; //计时器3归零
                counter2++; //计时器2加1
            }
            if (counter2 == 2) //如果计时器2的值累积到2了
            {
                counter2 = 0; //计时器2归零
                Destroy(toBeDestroyed); //销毁待销毁的圆环
                canDestroy = false; //能否销毁设为false
            }
        }

        //坦克移动到下一小关卡(例:从第一节地面的道路移到圆盘,坦克转弯,再从圆盘移到第二节地面的道路)
        if (canMove)
        {
            //计时器3开始计时
            timer3 += Time.deltaTime;
            if (timer3 >= 0.5)
            {
                //用插值让坦克的前方向往目的地的前方向旋转
                tank.transform.rotation = Quaternion.Lerp(tank.transform.rotation, destination.rotation, Time.deltaTime * 4);
                //如果坦克与目的地的角度差小于0.1f
                if (Quaternion.Angle(tank.transform.rotation, destination.rotation) < 0.1f)
                {
                    //用插值让坦克往目的地方向前进
                    tank.transform.position = Vector3.Lerp(tank.transform.position, destination.position, Time.deltaTime * 3);
                }
                //如果坦克与目的地的距离差小于0.1f
                if (Vector3.Distance(tank.transform.position, destination.position) < 0.1f)
                {
                    //如果移动变量等于小关卡数
                    if (mobileVariable == LittleLevel)
                    {
                        //移动变量加1
                        mobileVariable++;
                        //将目的地设为第mobileVariable节地面的道路中心
                        destination = transform.Find("SecondaryGround" + mobileVariable + "/Road");
                    }
                    else //否则
                    {
                        canMove = false; //能否移动设为false
                        timer3 = 0; //计时器3归零
                        //发送一个初始化地图事件
                        MVC.instance.SendEvent("InitializationMapEvent", null);
                    }
                }
            }
        }

        //如果动画状态机不为空
        if (animator != null)
        {
            //获取当前动画剪辑信息(第0层动画)
            AnimatorClipInfo[] animatorClipInfos = animator.GetCurrentAnimatorClipInfo(0);
            //如果当前动画剪辑信息的第0个动画剪辑的长度不为0
            if (animatorClipInfos.GetLength(0) != 0)
            {
                //取得动画剪辑信息中的第0个动画剪辑
                AnimatorClipInfo animatorClipInfo = animatorClipInfos[0];
                //如果此动画剪辑为受击动画
                if (animatorClipInfo.clip.name == "UnderAttackAnimation")
                {
                    //将动画状态机的布尔变量HitBool设为false
                    animator.SetBool("HitBool", false);
                }
                //如果此动画剪辑为胜利击动画
                else if (animatorClipInfo.clip.name == "VictoryAnimation")
                {
                    //将动画状态机的布尔变量WinBool设为false
                    animator.SetBool("WinBool", false);
                    //发送一个游戏结束事件,参数为字符串win(赢)
                    MVC.instance.SendEvent("GameOverEvent", "win");//发送一个游戏结束事件,参数为字符串win(赢)
                }
            }
        }
    }
}

加载关卡控制器的创建

  • 在Controller文件夹下新建一个脚本,命名为LoadLevelController
  • LoadLevelController类是用来生成地图中一开始就需要生成的对象
  • LoadLevelController类继承ControllerBase
  • 重写基类抽象方法
/// <summary>
/// LoadLevelEvent的处理程序
/// </summary>
public class LoadLevelController : ControllerBase
{
    public override void Execute(object param)
    {
        //强制转换参数
        int id = (int)param;

        //取得字典中的关卡数据合集
        LevelDatasModel lDsM = MVC.instance.models["LevelDatasModel"] as LevelDatasModel;

        //定义一个关卡数据类型的局部变量lDM
        LevelDataModel lDM = null;

        //遍历关卡数据数组
        for (int i = 0; i < lDsM.lDMS.Count; i++)
        {
            //如果关卡数据数组中的第i个元素的id等于局部变量id
            if (lDsM.lDMS[i].id == id)
            {
                //将此元素(关卡数据)赋给局部变量lDM
                lDM = lDsM.lDMS[i];
            }
        }

        //如果此关卡没有解锁
        if (lDM.isUnlock == false)
        {
            //返回,就不加载场景了
            return;
        }

        //生成地面
        //获取地面的名字
        string groundName = lDM.ground;
        //根据名字找到相应的预制体
        GameObject ground = Resources.Load("Prefabs/Grounds/" + groundName) as GameObject;
        //实例化地面预制体生成地面
        GameObject groundInstance = Object.Instantiate(ground);
        //把关卡数据赋给地面的脚本组件MainGroundView的成员变量lDM
        groundInstance.GetComponent<MainGroundView>().lDM = lDM;

        //将views字典中的MainGroundView删除()
        MVC.instance.views.Remove("MainGroundView");
        //注册刚生成出来的地面上的脚本组件MainGroundView
        MVC.instance.RegisterView(groundInstance.GetComponent<MainGroundView>());

        //以地面节数来循环(每一节地面上都需要生成一个财宝)
        for (int i = 0; i < lDM.groundPitchNumber; i++)
        {
            //获取关卡数据中的财宝数组中的第i个元素(财宝)的名字
            string treasureName = lDM.treasure[i];
            //根据名字获取财宝预制体
            GameObject treasure = Resources.Load("Prefabs/Treasures/" + treasureName) as GameObject;
            //实例化预制体(生成在第i节地面的圆盘的旋转中心1下)
            Object.Instantiate(treasure, groundInstance.transform.Find("SecondaryGround" + (i + 1) + "/Disc/RotationCenter1"));
        }

        //生成坦克
        //获取关卡数据中的坦克名字
        string tankName = lDM.tank;
        //根据名字获取预制体
        GameObject tank = Resources.Load("Prefabs/Tanks/"+ tankName) as GameObject;
        //实例化预制体(生成在第一节地面下)
        GameObject tankInstance = Object.Instantiate(tank, groundInstance.transform.Find("SecondaryGround1"));
        //将坦克的位置设置在第一节地面的道路上
        tankInstance.transform.position = groundInstance.transform.Find("SecondaryGround1/Road").position;
        //为地面的脚本组件MainGroundView的成员tank赋值
        groundInstance.GetComponent<MainGroundView>().tank = tankInstance;

        //将摄像机放置到坦克上方
        //找到摄像机
        GameObject camera = GameObject.Find("Main Camera");
        //将坦克设置为摄像机的父节点
        camera.transform.SetParent(tankInstance.transform);
        //设置摄像机的位置
        camera.transform.localPosition = new Vector3(0, 12, -12);
        //设置摄像机的旋转角度
        camera.transform.localRotation = new Quaternion();
        camera.transform.Rotate(30, 0, 0);
        //为地面的脚本组件MainGroundView的成员vidicon赋值
        groundInstance.GetComponent<MainGroundView>().vidicon = camera;

        //发送关卡加载成功事件,传入参数关卡id
        MVC.instance.SendEvent("LevelLoadSuccessEvent", id);
        //发送初始化地图事件
        MVC.instance.SendEvent("InitializationMapEvent", null);
    }
}
  • 在MainSceneManager脚本中注册LoadLevelController
MVC.instance.RegisterController("LoadLevelEvent", typeof(LoadLevelController)); //注册加载关卡控制器

发表评论