导入

前瞻

介绍

本次课程是基于保卫萝卜这个项目,以严格遵循MVC框架的形式去对项目进行复现。
重点学习方向——提高对于MVC框架和游戏模块的构建方式和使用场合的理解,提高对使用框架的必要性的感知,为未来开发商业项目做好准备。

页面预览

第1章: 需求与设计

设计思路

无序的数据对象


如上图,为游戏内一部分数据内容的排列,我们程序员的职责是将这些内容有机的结合起来,让它们进行合理的交互,最终将这些乱序的数据统合为面向玩家的有序的游戏形态

有序的游戏框架

而问题就常常出在统合为有序的过程之中了,大部分商业软件都是一项庞大且精密的功能,特别是游戏这一类软件的互动更是可以位于软件之首。

如果你不在一开始就敲定好代码的总体框架和大体逻辑,并严格遵循敲定好的规则。你会在开发中发现代码越来越难以维护和理解,最终变成了屎山代码。

通过两件事可以很大的避免这一结果的到来,其一为代码开发的过程中经常性的重构代码;其二便是我们这节课的重点,一开始就敲定好扩展性和维护性良好的框架,为游戏打好开发的根基。

本项目的应用层采取的就是程序界十分泛用的框架——MVC框架

MVC是三个单词的首字母缩写,它们是Model(模型)、View(视图)和Controller(控制)

MVC(应用层)

如上图所示,之前分散的数据已经被有序的划分到各大模块之中。

如果按照顺序来解释MVC分层的话:
1)最上面的一层,是直接面向最终用户的"视图层"(View)。它是提供给玩家的操作界面,是游戏的外壳。
2)最底下的一层,是核心的"数据层"(Model),也就是游戏需要操作的数据或信息。
3)中间的一层,就是"控制层"(Controller),它负责根据用户从"视图层"输入的指令,选取"数据层"中的数据,然后对其进行相应的操作,产生最终结果。

总结:视图层让玩家看,数据层让游戏程序读;视图层和数据层尽量解耦做到互不相干,视图层和数据层的交互交给控制层管

通过这种分层的写法,将游戏逻辑和游戏数据切割的明明白白,方便我们程序的开发和维护。

应用层和框架层

此外你应该还注意到了,MVC本身被划分到了应用层中,它的底下还有框架层的存在,如果理解这两个大层呢:

应用层:指的是针对游戏的功能实现定制化的一些内容,只不过这次我们定制化的时候打算遵循MVC框架的实现形式。在不同游戏应用层的变化都十分巨大,经常要对不同游戏重新设计合适的应用层。
在现实工作中,这一层常常是用来实现策划给你提出的功能。

框架层:将一些游戏里常用的代码功能而针对性的设计出了多个模块(即为工具类),用来应对大部分游戏都会需要的底层功能——如资源加载,声音产生。这一层的脚本轮子造好后,常常是可以在多个游戏复用的。
这一层的东西大多数以单例管理器的形式存在的,主要提供方法供应用层进行调用
这现实工作中,策划通常不会和你讲这些东西,是程序员自己用来减少代码量;进行优化和提高维护能力的功能脚本。

第2章: 框架层实现

前置

需要安装iTween插件:iTween | 动画 工具 | Unity Asset Store

注意,这里的框架层实现和我之前的文章:Unity程序基础框架 - 张先生的小屋 (klned.com)
非常的相似,建议比对着看,我也会对在下文两者进行比较。

单例类

单例模版Singleton类

用途

基于Mono继承的单例类,需要提前挂载到场景上。
好处是可以用挂载与否直接控制这个单例有没有必要被启用。

代码

Singleton:

/// <summary>  
/// 单例化已经挂载的Mono对象脚本,用来被继承使用  
/// </summary>  
public abstract class SingletonMono<T> : MonoBehaviour where T : MonoBehaviour  
{  
/// <summary>
/// 单例化已经挂载的Mono对象脚本,用来被继承使用
/// </summary>
public abstract class SingletonMono<T> : MonoBehaviour where T : MonoBehaviour
{

    
    private static T instance;

    public static T GetInstance()
    {
        //继承了Mono的脚本 不能够直接new
        //只能通过拖动到对象上 或者 通过 加脚本的api AddComponent去加脚本
        //U3D内部帮助我们实例化它
        return instance;
    }

    protected virtual void Awake()
    {
        instance = this as T;
    }

  
}

对象池(缓存池)

对象池原理

概述

类似于上篇文章的缓冲池

类设计图

大概功能:
缓存对象相关:
Ireusable:接口,用来供ReusableObject继承,将两个可供缓存对象实现的方法抽象化。
ReusableObject:抽象类,用来供缓存对象继承,将两个可供缓存对象实现的方法抽象化,这两个方法会在对象被取出或者放入缓存池的时候调用。

缓存池子相关:
SubPool:某一类对象的缓冲池,用来实现缓存对象和它所在的池子的交互,会以字典的value方式被ObjectPool调用。
ObjectPool:缓冲池管理类,负责管理缓存总池子,提供方法供外界调用。

对象池IReusable接口,ReusableObject类

用途

用来供缓存对象脚本继承,里面附带了缓存对象必须的两个方法,可以让缓存对象在进入游戏池子或者退出池子的时候进行执行,也方便程序员通过继承区别出哪些对象属于缓存对象的范围。

代码

IReusable接口:

public interface IReusable {  
  //将该对象取出缓冲池,该物品上脚本的该方法会被调用  
  void OnSpawn()  
 {     }  
  
//放入该对象进入缓冲池,该物品上脚本的该方法会被调用  
  void OnUnspawn()  
 {     }  
}

ReusableObject类:

public abstract class ReusableObject : MonoBehaviour,IReusable  
{  
  public abstract void OnSpawn();  
  
  public abstract void OnUnspawn();  
}

缓存对象使用例:

public class Cube : ReusableObject
{
    private void Update()
    {
    }
    
    public override void OnSpawn()
    {
        Debug.Log("被提出缓冲池会调用的方法");
    }

    public override void OnUnspawn()
    {
        Debug.Log("被压入缓冲池会调用的方法");
    }
}

总结

1.这两个脚本的存在是为了创造ReusableObject类用来让缓存对象继承用的。


2.需要注意的是,在之前一课的缓冲池里,是没有这两个脚本的。
使用了此脚本后,有利有弊:
利:
一.容易通过脚本对象的继承看出来这个脚本是缓存对象的脚本
二.存入和取出不再需要回调函数,而是将这两个方法直接由脚本对象实现。(请结合SubPool的SendMessage理解),更加符合面向对象思维。

弊:
1.不再那么灵活了,存入和取出的方法被写死到了缓存对象脚本上,而不是暴露给外界的回调想用就用——当然,你依然可以补写一个回调函数实现这个功能。
2.SendMessage方法是比较损耗性能的。

对象小池SubPool类

用途

用来存储单独一类对象的”小池子“,

代码

//同类型小池子
public class SubPool
{
    //被缓存到池子存储的对象
    private List<GameObject> objs=new List<GameObject>();
    
    //池子名,也用来给存储的对象命名
    private string name;
    
    //预载对象,用来当池子物品不够时,实例化对象。
    private GameObject prefabobj;
    
    //当存入缓冲池失活时,对象挂载的父节点
    public GameObject fatherObj;
    
    /// <summary>
    /// 用来初始化同类型小池子
    /// </summary>
    /// <param name="name">此池子名字</param>
    /// <param name="obj">此池子的实例预载对象</param>
    /// <param name="pool">缓冲池的总父对象,用来让缓存池更直观</param>
  public SubPool(string name,GameObject obj,GameObject pool)
  {
      prefabobj = obj;
      this.name = name;
      
      //初始化父节点
      fatherObj = new GameObject(name);
fatherObj.transform.SetParent(pool.transform);
  }

/// <summary>
/// 取出缓存池内的某个对象
/// </summary>
   public GameObject Spawn()
{
    GameObject go=null;
    //先看池子有没有,有就拿出来
    foreach (var obj in objs)
    {
        if (!obj.activeSelf)
        {
            go = obj;
        }
    }

    //如果没有,直接新建一个
    if (go == null)
    {
        go = GameObject.Instantiate(prefabobj);
        go.name = this.name;
        objs.Add(go);
    }

    go.SetActive(true);
    go.SendMessage("OnSpawn",SendMessageOptions.DontRequireReceiver);
    
    //将对象从缓冲池里取出
    go.transform.SetParent(null);
    return go;
}
   
/// <summary>
/// 将一个对象存入缓冲池(前提是该对象属于此缓冲池创建出来的)
/// </summary>
/// <param name="go">存入的对象</param>
   public void Unspawn(GameObject go)
   {
       if(objs.Contains(go)){
           go.SendMessage("OnUnspawn",SendMessageOptions.DontRequireReceiver);   
        go.SetActive(false);
    
    //将对象放入缓冲池
    go.transform.SetParent(fatherObj.transform);
       }
   }
   
/// <summary>
/// 将此类池子的对象全部失活存入缓存池
/// </summary>
   public void UnspawnAll()
   {
       foreach (var obj in objs)
       {
           if (obj.activeSelf)
           {
               Unspawn(obj);
           }
       }
   }
}

总结

于上一课不同的是,这里我们将所有的缓存对象都存进List后不再取出,而是直接通过遍历来执行放入和取出操作,缺点是性能会有一定的损耗,优点是可以方便的执行回收所有缓存对象的UnspawnAll方法。

对象池管理类ObjectPool类

用途

用来提供接口供外界管理对象池的管理单例类。

代码

public class ObjectPool : SingletonMono<ObjectPool>
{
 //对象池总父类,主要用来在面板里面方便查看
 private GameObject pool=null;

 //存储的各类型池子
 private Dictionary<string, SubPool> dic = new Dictionary<string, SubPool>();

//资源路径,可以用默认值自定义路径前缀  
private String ResourceDir="Res";
 
 protected override void Awake()
 {
  base.Awake();
  pool = new GameObject("pool");//创建父类对象
 }

 /// <summary>
 /// 取出池子中的对象
 /// </summary>
 /// <param name="name"></param>
 public void Spawn(string name)
 {
  if (!dic.ContainsKey(name))
  {
   RegisterNew(name);
  }

  dic[name].Spawn();
 }

 /// <summary>
 /// 压入对象到池子中
 /// </summary>
 /// <param name="go"></param>
 public void Unspawn(GameObject go)
 {
  if (!dic.ContainsKey(go.name))
  {
   Debug.Log("不存在此池子!");
   return;
  }

  dic[go.name].Unspawn(go);
 }

 /// <summary>
 /// 回收所有缓冲池对象
 /// </summary>
 public void UnspawnAll()
 {
  foreach (SubPool p in dic.Values)
   p.UnspawnAll();
 }

 /// <summary>
 /// 创建新的一类池子
 /// </summary>
 /// <param name="name">池子名(和Res目录路径同名)</param>
 private void RegisterNew(String name)
 {
  //预设路径
  string path = "";
  //如果路径为空,说明没有自定义路径,直接用
  if (string.IsNullOrEmpty(ResourceDir.Trim()))
   path = name;
  else
   path = ResourceDir + "/" + name;
  
  SubPool newpool= new SubPool(name, Resources.Load<GameObject>(path), pool);
  dic.Add(name,newpool);
 }


}

总结

比之前一课的相同类多出了变量ResourceDir(资源路径),可以在用RegisterNew方法创建新池子的Resources路径加上前缀,以让缓存对象的命名更加简洁(原来是Obj/Name,通过设置ResourceDir就可以直接变成Name)。

声音管理器

声音管理器SoundMgr

用途

用来管理游戏中的背景音乐和音效列表。

代码

SoundMgr :

public class SoundMgr : SingletonMono<SoundMgr>
{
    //资源路径,可以用默认值自定义路径前缀
    private string ResourceDir="Sound";
    
    //挂载声音和音效的对象
    private GameObject Soundobj = null;
    
    //音乐组件
    //唯一的背景音乐
    private AudioSource bkSource;
    //唯一的音效组件
    private AudioSource soundSource;

    //背景音乐大小相关
    //大小
    private float bkValue=1;
    //是否开启
    private bool bkBool=true;
    
    
    //音效大小相关
    //大小
    private float soundValue=1;
    //是否开启
    private bool soundBool=true;

    protected override void Awake()
    {
        base.Awake();
        //创建挂载对象
        Soundobj = new GameObject("Sound");
        //挂载组件
        bkSource= Soundobj.AddComponent<AudioSource>();
        soundSource = Soundobj.AddComponent<AudioSource>();
        bkSource.loop = true;
    }

    
    
   /// <summary>
   /// 播放背景音乐
   /// </summary>
   /// <param name="audioName">音乐名(路径)</param>
    public void PlayBg(string audioName)
   {
       //如果一样,就重播
       if (bkSource.clip!=null&&bkSource.clip.name == audioName)
       {
           bkSource.Play();
           return;
       }

       //预设路径
       string path = "";
       //如果路径为空,说明没有自定义路径,直接用
       if (string.IsNullOrEmpty(ResourceDir.Trim()))
           path = audioName;
       else
           path = ResourceDir + "/" + audioName;


       bkSource.clip=Resources.Load<AudioClip>(path);
       bkSource.mute = !bkBool;
       bkSource.volume = bkValue;
       bkSource.Play();
   }

    /// <summary>
    /// 停止背景音乐
    /// </summary>
    public void StopBg()
    {
        bkSource.Stop();
        bkSource.clip = null;
    }

    /// <summary>
    /// 播放音效
    /// </summary>
    /// <param name="audioName">音效名(路径)</param>
    public void PlayEffect(string audioName)
    {
        //预设路径
        string path = "";
        //如果路径为空,说明没有自定义路径,直接用
        if (string.IsNullOrEmpty(ResourceDir.Trim()))
            path = audioName;
        else
            path = ResourceDir + "/" + audioName;
    
        soundSource.mute = !soundBool;
        soundSource.volume = soundValue;
        //使用PlayOneShot快捷的实现音效系统
        soundSource.PlayOneShot(Resources.Load<AudioClip>(path));
    }
}

总结

1.实现和之前的音乐管理器有很大的不同,不同于,音效使用PlayOneShot简便的实现了,这样我们就完全不需要通过update去检测是否播完了——但是上一课里面唐老狮没有指出这种用法?也许是他忘了有这种方法。

2.关于PlayOneShot:此方法可以通过唯一个组件就实现一个组件同时播放多个音效
当然,这样写有个3个缺点:
1.它无法循环播放音效,只能播放一次。如果想要循环播放音效,就需要使用Play方法或者设置AudioSource的loop属性为true。

2.另外,这个方法也不能控制音效的播放位置,只能从头开始播放。如果想要从音效的某个时间点开始播放,就需要使用PlayScheduled方法或者设置AudioSource的time属性。

3.相比之前的写法,这种写法难以扩展,倘若我们开发商业游戏,有的游戏音效太过繁杂,为了玩家体验,我们经常得设计一些权重系统剔除掉多余的小音乐,如果使用这个方法基本就无法进行扩展了。

所以,综上所述,此方法仅仅适用于音效数量小的游戏项目。

MVC框架

MVC框架原理

概述

MVC分别代表Model - View - Controller
Model - 模型层,也就是说,他只负责数据和提醒视图层更新。
View - 视图层,也就是说,他只负责更新UI和设置触发事件。
Controller - 控制层,他只负责控制,并关联Model和View

本文框架的写法

请注意,MVC框架的写法多种多样,要结合项目去实现,有时候会适当的砍掉一些功能,有时候会加上一些功能,所以看到别人的mvc框架不同也不要感到疑惑,下面的内容仅仅介绍我们本文的mvc实现思路。

这次我们的mvc广泛的使用了观察者模式,你可以把这个框架和我上一课的事件中心结合起来理解。

为了让mvc更加适应游戏开发流程,并进一步降低耦合度(MVC原版的3个框架经常还是需要彼此调用的,我们不妨把彼此调用改为3个框架统一调用MVC里面的公共方法),所以我们为MVC准备了一个中间层MVC脚本类,这个类主要用来帮忙存储事件以及提供调用接口供三个类调用。

总体思路:
1.开局运用mvc层注册一个启动控制器,这个控制器实现将model层所有的数据对象以及controller层的类型(controller层和view层当需要的时候才实例化,因为经常涉及到获取不同场景的对象)存入mvc层方便调用 。
2.当前需要的View层准备好对应前段后用户供点击操作。
3.用户操作后view层向mvc类发送触发事件方法,该方法包括事件名,MVC类调用对应名称的事件,事件会触发在开局挂载的有对应名称的事件,事件触发后实例化此符合事件名的控制器进行触发。
4.触发后先调用对应事件名的控制器,控制器可能会对model层做对应的修改以及处理游戏逻辑,如果model层有涉及到数据修改,需要再次发送事件去促使view层对页面进行更新,view层便可以获取对应的model相对应的改变。(model层自己的是不需要挂载事件处理逻辑的,最多在发生修改后告诉view层更新,相当于只是给供人调用的数据存储中心,最多预留好修改数据和获取数据的方法让控制器和视图层调用即可)


无视MVC中间类的简写版:

  • View触发事件
  • Controller处理业务逻辑触发数据更新
  • 更新Model的数据
  • Model带着数据回到View
  • View更新数据

MVC类

用途

作为MVC框架更方便实现的静态类。

代码

前置代码(MVC三个父类,会在后文完善):

View:

public abstract class View : MonoBehaviour
{
    //视图标识
    public abstract string Name { get; }

    //关心的事件列表
    [HideInInspector]
    public List<string> AttentionEvents = new List<string>();

    //事件处理函数
    public abstract void HandleEvent(string eventName, object data);
}

Controller:

public abstract class Controller
{
    //处理系统消息    
    public abstract void Execute(object data);

}

Model:

public abstract class Model
{
    public abstract string Name { get; }

}
mvc类

MVC类:

public static class MVC
{
    //存储MVC
    public static Dictionary<string, Model> Models = new Dictionary<string, Model>(); //名字---模型
    public static Dictionary<string, View> Views = new Dictionary<string, View>();//名字---视图
    public static Dictionary<string, Type> CommandMap = new Dictionary<string, Type>();//事件名字---控制器类型

    //注册
    public static void RegisterModel(Model model)
    {
        Models[model.Name] = model;
    }

    public static void RegisterView(View view)
    {
        //防止重复注册
        if (Views.ContainsKey(view.Name))
            Views.Remove(view.Name);

        //注册关心的事件
        view.RegisterEvents();

        //缓存
        Views[view.Name] = view;
    }

    public static void RegisterController(string eventName, Type controllerType)
    {
        CommandMap[eventName] = controllerType;
    }

    //获取
    public static T GetModel<T>()
        where T : Model
    {
        foreach (Model m in Models.Values)
        {
            if (m is T)
                return (T)m;
        }
        return null;
    }

    public static T GetView<T>()
        where T : View
    {
        foreach (View v in Views.Values)
        {
            if (v is T)
                return (T)v;
        }
        return null;
    }

    //发送事件
    public static void SendEvent(string eventName, object data = null)
    {
        //控制器响应事件
        if(CommandMap.ContainsKey(eventName))
        {
            Type t = CommandMap[eventName];
            Controller c = Activator.CreateInstance(t) as Controller;
            //控制器执行
            c.Execute(data);
        }

        //视图响应事件
        //查看是否有View(UI)关注这个事件的触发?
        //有就执行view(UI)里面的HandleEvent(大部分是UI更新功能)
        foreach(View v in Views.Values)
        {
            if(v.AttentionEvents.Contains(eventName))
            {
                //视图响应事件
                v.HandleEvent(eventName, data);
                
            }
        }
    }
}

总结

本项目的mvc不止有mvc三层,还多了一个mvc静态脚本,它本身其实不参与任何逻辑,只是把三层里面的都需要用的方法提取出来供三层调用,充当中介作用。

原版的mvc还是带有一定的耦合性——因为其中一层是直接调用其他两层的方法(调用其他层的方法实现事件提醒或者获取model对象上的数据),如果其他层的方法名字或者功能发生了变化,常常需要三层一起改。
而现在我们引入mvc中间类,用字典的形式来装载实例,然后通过事件名调用实例对应里面的事件,进一步解耦了它们之间的关系,视图层和控制层在自己层内把方法写挂载到事件池里面,它们两层有变化的时候修改下注册事件代码即可。

Model类

用途

用来供存储数据的数据层父类。

代码

Model:

public abstract class Model
{
    public abstract string Name { get; }
    //通知功能
    protected void SendEvent(string eventName, object data = null)
    {
        MVC.SendEvent(eventName, data);
    }
}

总结

本层并不需要调用其他层的类,而是作为数据存储中心供其他层调用或者通知其他层,所以只需要提供一个通知方法即可。

View类

用途

用来处理面向用户的前端一层的父类

代码

View:

public abstract class View : MonoBehaviour
{
    //视图标识
    public abstract string Name { get; }

    //关心的事件列表
    [HideInInspector]
    public List<string> AttentionEvents = new List<string>();

    //注册关心的事件(当该类被注册时会调用该方法)
    public virtual void RegisterEvents()
    {

    }

    //事件处理函数
    public abstract void HandleEvent(string eventName, object data);

    //获取模型
    protected T GetModel<T>()
        where T : Model
    {
        return MVC.GetModel<T>() as T;
    }

    //发送消息
    protected void SendEvent(string eventName,object data=null)
    {
        MVC.SendEvent(eventName, data);
    }
}

总结

本类存有事件字典AttentionEvents ,待其他层调用对应名字的事件提醒方法,该类的注册的函数便会执行UI更新。

由于view层的更新需要获取model里的数据,所以给予该层获取model的方法GetModel。

本层的UI在用户交互后可能需要通知控制器进行逻辑处理,所以给与该层发送提醒消息的方法SendEvent。

为自己挂载注册事件的RegisterEvents可重写方法。(当用mvc注册的时候会自动调用)

Controller类

用途

用来创建控制器实例的父类,具有承上启下的功能。

代码

Controller:

public abstract class Controller
{
    //获取模型
    protected T GetModel<T>()
        where T:Model
    {
        return MVC.GetModel<T>() as T;
    }

    //获取视图
    protected T GetView<T>()
        where T : View
    {
        return MVC.GetView<T>() as T;
    }

    protected void RegisterModel(Model model)
    {
        MVC.RegisterModel(model);
    }

    protected void RegisterView(View view)
    {
        MVC.RegisterView(view);
    }

    protected void RegisterController(string eventName,Type controllerType)
    {
        MVC.RegisterController(eventName, controllerType);
    }

    //自己被触发后调用的方法  
    public abstract void Execute(object data);
}

总结

1.控制器是很万能的存在,为了解耦,你可以把其他两层的注册步骤也全权交给他,它还可以充当事件中心用来干一些游戏里纯逻辑的事情,而不需要和其他两层交互——控制器可以离开其他两层,其他两层离不开它。

2.控制器和其他两层不太一样,它是一个即用即抛的事件触发仪器,只有在需要的时候创建出来调用Execute方法,调用完成后便没用了。而不像view和model会长期存在的游戏中。

2.由于控制器充当其他两层的交互桥梁,我们自然给予了它:

  • 获取其他两层实例的能力:GetModel和GetView
  • 为其他两层注册的能力:RegisterModel和RegisterView
  • 为自己注册的能力(当然):RegisterController
  • 当自己被提醒触发事件时,调用Execute方法。

ApplicationBase类

用途

上面的框架构建完毕后,你会发现虽然逻辑已经跑通了,但是除了View之外,其他类甚至不是mono类,没有办法启动游戏这个“死循环”和进行初始化,而View层是专门处理前端的,最好不要破坏其解耦性。
所以,我们设计充当MVC框架的启动父类ApplicationBase,负责游戏内容的初始化。

代码

ApplicationBase:

public abstract class ApplicationBase<T> : SingletonMono<T>
    where T : MonoBehaviour
{
    //注册控制器
    protected void RegisterController(string eventName,Type controllerType)
    {
        MVC.RegisterController(eventName, controllerType);
    }

    protected void SendEvent(string eventName, object data = null)
    {
        MVC.SendEvent(eventName, data);
    }
}

总结

基础于Mono,拥有注册注册器和触发事件的能力。有了这个父类,它的子类就可以轻松的借助创建和触发一个初始化控制器来实现代码的初始化。

第3章: 地图编辑器

介绍编辑器

由于保卫萝卜的关卡本质上都是高度重复的,我们不妨设计一个地图编辑器,来达到不需要关注代码的情况下设计关卡,然后使用xml存储信息最后进行读取,增加我们的游戏制作效率。

编辑器的使用方式

当我们在编辑器场景运行编译器的时候就会进入编辑器状态,编辑器会帮我们切割好格子供我们点击,鼠标左键设计怪物路径,鼠标右键设计炮塔点,背景图是预设好的图片。

设计好地图后,我们有一个Map脚本充当控制器,可以在上面保存和读取关卡,并允许对当前的场景进行清除。

实现Point,Tile,Round类

用途

这一节主要是实现和关卡有关的信息类。
Point:代表编辑器中的格子坐标(点)。
Tite:代表编辑器中的格子。
Round:代表怪物波次信息

代码

Point:

//格子坐标
public class Point 
{
    public int X;
    public int Y;


    public Point(int x, int y)
    {
        this.X = x;
        this.Y = y;
    }
}

Tile:

//格子信息
public class Tile
{
    public int X;
    public int Y;
    public bool CanHold; //是否可以放置塔
    public object Data; //格子所保存的数据

    public Tile(int x, int y)
    {
        this.X = x;
        this.Y = y;
    }

    public override string ToString()
    {
        return string.Format("[X:{0},Y:{1},CanHold:{2}]",
            this.X,
            this.Y,
            this.CanHold
        );
    }
}

Round:

//怪物波次信息
public class Round
{
    public int Monster; //怪物类型ID
    public int Count;   //怪物数量

 
    public Round(int monster, int count)
    {
        this.Monster = monster;
        this.Count = count;
    }
}

总结

创建好各大信息类,用来为功能类做数据调用的准备。

实现Level,Tools类

用途

Consts:名字记录类,用来记录一些命名或者路径有关的数据,方便引用修改和进行统一管理。
Level:关卡信息类,用来记录关卡信息。
Tools:工具类,用来提供读取和保存关卡信息的方法。

代码

Consts(这里给出的是最终版本):

public static class Consts
{
    //目录
    public static readonly string LevelDir = Application.dataPath + @"\Game\Resources\Res\Levels";
    public static readonly string MapDir = Application.dataPath + @"\Game\Resources\Res\Maps";
    public static readonly string CardDir = Application.dataPath + @"\Game\Resources\Res\Cards";

    //参数
    public const string GameProgress = "GameProgress";
    public const float DotClosedDistance = 0.1f;
    public const float RangeClosedDistance = 0.7f;

    //Model
    public const string M_GameModel = "M_GameModel";
    public const string M_RoundModel = "M_RoundModel";


    //View
    public const string V_Start = "V_Start";
    public const string V_Select = "V_Select";
    public const string V_Board = "V_Board";
    public const string V_CountDown = "V_CountDown";
    public const string V_Win = "V_Win";
    public const string V_Lost = "V_Lost";
    public const string V_Sytem = "V_Sytem";
    public const string V_Complete = "V_Complete";
    public const string V_Spanwner = "V_Spanwner";
    public const string V_TowerPopup = "V_TowerPopup";
    
    
    //Controller
    public const string E_StartUp = "E_StartUp";

    public const string E_EnterScene = "E_EnterScene"; //SceneArgs
    public const string E_ExitScene = "E_ExitScene";//SceneArgs

    public const string E_StartLevel = "E_StartLevel"; //StartLevelArgs
    public const string E_EndLevel = "E_EndLevel";//EndLevelArgs

    public const string E_CountDownComplete = "E_CountDownComplete";

    public const string E_StartRound = "E_StartRound";//StartRoundArgs
    public const string E_SpawnMonster = "E_SpawnMonster";//SpawnMonsterArgs
    public const string E_SpawnTower = "E_SpawnTower";//SpawnTowerArgs
    public const string E_UpgradeTower = "E_UpgradeTower";//UpgradeTowerArgs
    public const string E_SellTower = "E_SellTower";//SellTowerArgs

    public const string E_ShowCreate = "E_ShowCreate";//ShowCreatorArgs
    public const string E_ShowUpgrade = "E_ShowUpgrade";//ShowUpgradeArgs
    public const string E_HidePopup = "E_HidePopup";
}

public enum GameSpeed
{
    One,
    Two
}

public enum MonsterType
{
    Monster0,
    Monster1,
    Monster2,
    Monster3,
    Monster4,
    Monster5
}

Level:

public class Level
{
    //名字
    public string Name;

    //卡片
    public string CardImage;

    //背景
    public string Background;

    //路径
    public string Road;

    //金币
    public int InitScore;

    //炮塔可放置的位置
    public List<Point> Holder = new List<Point>();

    //怪物行走的路径
    public List<Point> Path = new List<Point>();

    //出怪回合信息
    public List<Round> Rounds = new List<Round>();
}

Tools:

public class Tools 
{
    //读取关卡列表
    public static List<FileInfo> GetLevelFiles()
    {
        string[] files = Directory.GetFiles(Consts.LevelDir, "*.xml");

        List<FileInfo> list = new List<FileInfo>();
        for (int i = 0; i < files.Length; i++)
        {
            FileInfo file = new FileInfo(files[i]);
            list.Add(file);
        }
        return list;
    }

    //填充传入的Level类数据
    public static void FillLevel(string fileName, ref Level level)
    {
        FileInfo file = new FileInfo(fileName);
        StreamReader sr = new StreamReader(file.OpenRead(), Encoding.UTF8);

        XmlDocument doc = new XmlDocument();
        doc.Load(sr);

        level.Name = doc.SelectSingleNode("/Level/Name").InnerText;
        level.CardImage = doc.SelectSingleNode("/Level/CardImage").InnerText;
        level.Background = doc.SelectSingleNode("/Level/Background").InnerText;
        level.Road = doc.SelectSingleNode("/Level/Road").InnerText;
        level.InitScore = int.Parse(doc.SelectSingleNode("/Level/InitScore").InnerText);

        XmlNodeList nodes;

        nodes = doc.SelectNodes("/Level/Holder/Point");
        for (int i = 0; i < nodes.Count; i++)
        {
            XmlNode node = nodes[i];
            Point p = new Point(
                int.Parse(node.Attributes["X"].Value),
                int.Parse(node.Attributes["Y"].Value));

            level.Holder.Add(p);
        }

        nodes = doc.SelectNodes("/Level/Path/Point");
        for (int i = 0; i < nodes.Count; i++)
        {
            XmlNode node = nodes[i];

            Point p = new Point(
                int.Parse(node.Attributes["X"].Value),
                int.Parse(node.Attributes["Y"].Value));

            level.Path.Add(p);
        }

        nodes = doc.SelectNodes("/Level/Rounds/Round");
        for (int i = 0; i < nodes.Count; i++)
        {
            XmlNode node = nodes[i];

            Round r = new Round(
                    int.Parse(node.Attributes["Monster"].Value),
                    int.Parse(node.Attributes["Count"].Value)
                );

            level.Rounds.Add(r);
        }

        sr.Close();
        sr.Dispose();
    }

    //保存关卡
    public static void SaveLevel(string fileName, Level level)
    {
   StringBuilder sb = new StringBuilder();
        sb.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        sb.AppendLine("<Level>");

        sb.AppendLine(string.Format("<Name>{0}</Name>", level.Name));
        sb.AppendLine(string.Format("<CardImage>{0}</CardImage>", level.CardImage));
        sb.AppendLine(string.Format("<Background>{0}</Background>", level.Background));
        sb.AppendLine(string.Format("<Road>{0}</Road>", level.Road));
        sb.AppendLine(string.Format("<InitScore>{0}</InitScore>", level.InitScore));

        sb.AppendLine("<Holder>");
        for (int i = 0; i < level.Holder.Count; i++)
        {
            sb.AppendLine(string.Format("<Point X=\"{0}\" Y=\"{1}\"/>", level.Holder[i].X, level.Holder[i].Y));
        }
        sb.AppendLine("</Holder>");

        sb.AppendLine("<Path>");
        for (int i = 0; i < level.Path.Count; i++)
        {
            sb.AppendLine(string.Format("<Point X=\"{0}\" Y=\"{1}\"/>", level.Path[i].X, level.Path[i].Y));
        }
        sb.AppendLine("</Path>");

        sb.AppendLine("<Rounds>");
        for (int i = 0; i < level.Rounds.Count; i++)
        {
            sb.AppendLine(string.Format("<Round Monster=\"{0}\" Count=\"{1}\"/>", level.Rounds[i].Monster, level.Rounds[i].Count));
        }
        sb.AppendLine("</Rounds>");

        sb.AppendLine("</Level>");

        string content = sb.ToString();


        XmlWriterSettings settings = new XmlWriterSettings();
        settings.Indent = true;
        settings.ConformanceLevel = ConformanceLevel.Auto;
        settings.IndentChars = "\t";
        settings.OmitXmlDeclaration = false;

        XmlWriter xw =XmlWriter.Create(fileName,settings);
        
        XmlDocument doc = new XmlDocument();
        doc.LoadXml(content);
        doc.WriteTo(xw);

        xw.Flush();
        xw.Close();


    }

    //加载图片
    public static IEnumerator LoadImage(string url, SpriteRenderer render)
    {
        WWW www = new WWW(url);

        while (!www.isDone)
            yield return www;

        Texture2D texture = www.texture;
        Sprite sp = Sprite.Create(
            texture,
            new Rect(0, 0, texture.width, texture.height),
            new Vector2(0.5f, 0.5f));
        render.sprite = sp;
    }

    public static IEnumerator LoadImage(string url, Image image)
    {
        WWW www = new WWW(url);

        while (!www.isDone)
            yield return www;

        Texture2D texture = www.texture;
        Sprite sp = Sprite.Create(
            texture,
            new Rect(0, 0, texture.width, texture.height),
            new Vector2(0.5f, 0.5f));
        image.sprite = sp;
    }
}

总结

Tools方法里面涉及到保存和读取xml的时候没有用数据流直接读取,而是设置了一个String对象一句一句的构建,这会导致看起来代码很臃肿,但是却可以保证性能更好。
当然涉及到大量的xml读取和保存的时候还是建议用数据流直接读取。

编辑器主体类导入

介绍

前置已经完成,接下来我们要开始编辑器有关主体类的设计,这也是逻辑最为复杂的部分,要设计两个新脚本,一个是运行时期处理页面显示和编辑逻辑的Map类,一个是提供一个编辑器供我们存储和选择关卡的MapEditor编辑器类。

类图

编辑器主类–Map类

介绍

编辑器场景运行时执行的类,主要处理关卡设计师和关卡的交互逻辑。

代码

前置:
供鼠标点击调用的参数类TileClickEventArgs :

//鼠标点击参数类
public class TileClickEventArgs : EventArgs
{
    public int MouseButton; //0左键,1右键
    public Tile Tile;

    public TileClickEventArgs(int mouseButton, Tile tile)
    {
        this.MouseButton = mouseButton;
        this.Tile = tile;
    }
}

Map:

//用于描述一个关卡地图的状态
public class Map : MonoBehaviour
{
    #region 常量
    public const int RowCount 		= 8;  //行数
    public const int ColumnCount 	= 12; //列数
    #endregion

    #region 事件
    public event EventHandler<TileClickEventArgs> OnTileClick;
    #endregion

    #region 字段
    float MapWidth;//地图宽
    float MapHeight;//地图高

    float TileWidth;//格子宽
    float TileHeight;//格子高

    Level m_level; //关卡数据

    List<Tile> m_grid = new List<Tile>(); //格子集合
    List<Tile> m_road = new List<Tile>(); //路径集合

    public bool DrawGizmos = true; //是否绘制网格
    #endregion

    #region 属性

    public Level Level
    {
        get { return m_level; }
    }

    public string BackgroundImage
    {
        set
        {
            SpriteRenderer render = transform.Find("Background").GetComponent<SpriteRenderer>();
            StartCoroutine(Tools.LoadImage(value, render));
        }
    }

    public string RoadImage
    {
        set
        {
            SpriteRenderer render = transform.Find("Road").GetComponent<SpriteRenderer>();
            StartCoroutine(Tools.LoadImage(value, render));
        }
    }

    public Rect MapRect
    {
        get { return new Rect(-MapWidth / 2, -MapHeight / 2, MapWidth, MapHeight); }
    }

    public List<Tile> Grid
    {
        get { return m_grid; }
    }

    public List<Tile> Road
    {
        get { return m_road; }
    }

    //怪物的寻路路径(获取的时候把格子行列转换为坐标)
    public Vector3[] Path
    {
        get
        {
            List<Vector3> m_path = new List<Vector3>();
            for (int i = 0; i < m_road.Count; i++)
            {
                Tile t = m_road[i];
                Vector3 point = GetPosition(t);
                m_path.Add(point);
            }
            return m_path.ToArray();
        }
    }

    #endregion

    #region 方法
    public void LoadLevel(Level level)
    {
        //清除当前状态
        Clear();

        //保存
        this.m_level = level;

        //加载图片
        this.BackgroundImage = "file://" + Consts.MapDir + "/" + level.Background;
        this.RoadImage = "file://" + Consts.MapDir + "/" + level.Road;

        //寻路点
        for (int i = 0; i < level.Path.Count; i++)
        {
            Point p = level.Path[i];
            Tile t = GetTile(p.X, p.Y);
            m_road.Add(t);
        }

        //炮塔点
        for (int i = 0; i < level.Holder.Count; i++)
        {
            Point p = level.Holder[i];
            Tile t = GetTile(p.X, p.Y);
            t.CanHold = true;
        }
    }

    //清除塔位信息
    public void ClearHolder()
    {
        foreach (Tile t in m_grid)
        {
            if(t.CanHold)
                t.CanHold = false;
        }
    }

    //清除寻路格子集合
    public void ClearRoad()
    {
        m_road.Clear();
    }

    //清除所有信息
    public void Clear()
    {
        m_level = null;
        ClearHolder();
        ClearRoad();
    }

    #endregion

    #region Unity回调
    //只在运行期起作用
    void Awake()
    {
        //计算地图和格子大小
        CalculateSize();

        //创建所有的格子
        for (int i = 0; i < RowCount; i++)
            for (int j = 0; j < ColumnCount; j++)
                m_grid.Add(new Tile(j, i));

        //监听鼠标点击事件
        OnTileClick += Map_OnTileClick;
    }

    void Update()
    {
        //鼠标左键检测
        if (Input.GetMouseButtonDown(0))
        {
            Tile t = GetTileUnderMouse();
            if (t != null)
            {
                //触发鼠标左键点击事件
                TileClickEventArgs e = new TileClickEventArgs(0, t);
                if (OnTileClick != null)
                    OnTileClick(this, e);
            }
        }

        //鼠标右键检测
        if (Input.GetMouseButtonDown(1))
        {
            Tile t = GetTileUnderMouse();
            if (t != null)
            {
                //触发鼠标右键点击事件
                TileClickEventArgs e = new TileClickEventArgs(1, t);
                if (OnTileClick != null)
                    OnTileClick(this, e);
            }
        }
    }

    //只在编辑器里起作用
    void OnDrawGizmos()
    {
        if (!DrawGizmos)
            return;

        //计算地图和格子大小
        CalculateSize();

        //格子颜色
        Gizmos.color = Color.green;

        //绘制行
        for (int row = 0; row <= RowCount; row++)
        {
            Vector2 from = new Vector2(-MapWidth / 2, -MapHeight / 2 + row * TileHeight);
            Vector2 to = new Vector2(-MapWidth / 2 + MapWidth, -MapHeight / 2 + row * TileHeight);
            Gizmos.DrawLine(from, to);
        }

        //绘制列
        for (int col = 0; col <= ColumnCount; col++)
        {
            Vector2 from = new Vector2(-MapWidth / 2 + col * TileWidth, MapHeight / 2);
            Vector2 to = new Vector2(-MapWidth / 2 + col * TileWidth, -MapHeight / 2);
            Gizmos.DrawLine(from, to);
        }

        //绘制格子
        foreach (Tile t in m_grid)
        {
            if (t.CanHold)
            {
                Vector3 pos = GetPosition(t);
                Gizmos.DrawIcon(pos, "holder.png", true);
            }
        }

        Gizmos.color = Color.red;
        for (int i = 0; i < m_road.Count; i++)
        {
            //起点
            if (i == 0)
            {
                Gizmos.DrawIcon(GetPosition(m_road[i]), "start.png", true);
            }

            //终点
            if (m_road.Count > 1 && i == m_road.Count - 1)
            {
                Gizmos.DrawIcon(GetPosition(m_road[i]), "end.png", true);
            }

            //红色的连线
            if (m_road.Count > 1 && i != 0)
            {
                Vector3 from = GetPosition(m_road[i - 1]);
                Vector3 to = GetPosition(m_road[i]);
                Gizmos.DrawLine(from, to);
            }
        }
    }
    #endregion

    #region 事件回调
    void Map_OnTileClick(object sender, TileClickEventArgs e)
    {
        //当前场景不是LevelBuilder不能编辑
        if (gameObject.scene.name != "LevelBuilder")
            return;

        if (Level == null)
            return;

        //处理放塔操作
        if (e.MouseButton == 0 && !m_road.Contains(e.Tile))
        {
            e.Tile.CanHold = !e.Tile.CanHold;
        }
        
        //处理寻路点操作
        if (e.MouseButton == 1 && !e.Tile.CanHold)
        {
            if (m_road.Contains(e.Tile))
                m_road.Remove(e.Tile);
            else
                m_road.Add(e.Tile);
        }
    }
    #endregion

    #region 帮助方法
    //计算地图大小,格子大小
    void CalculateSize()
    {
        Vector3 leftDown = new Vector3(0, 0);
        Vector3 rightUp = new Vector3(1, 1);

        Vector3 p1 = Camera.main.ViewportToWorldPoint(leftDown);
        Vector3 p2 = Camera.main.ViewportToWorldPoint(rightUp);

        MapWidth = Math.Abs(p2.x - p1.x);
        MapHeight = Math.Abs(p2.y - p1.y);

        TileWidth = MapWidth / ColumnCount;
        TileHeight = MapHeight / RowCount;
    }

    //获取格子中心点所在的世界坐标
    public Vector3 GetPosition(Tile t)
    {
        return new Vector3(
            //由于画布的正中间坐标是0,0 。所以我们要减去左边。
                -MapWidth / 2 + (t.X + 0.5f) * TileWidth,
                -MapHeight / 2 + (t.Y + 0.5f) * TileHeight,
                0
            );
    }

    //根据格子索引号获得格子
    public Tile GetTile(int tileX, int tileY)
    {
        int index = tileX + tileY * ColumnCount;
        if (index < 0 || index >= m_grid.Count)
            throw new IndexOutOfRangeException("格子索引越界");
        return m_grid[index];
    }

    //获取所在位置获得格子
    public Tile GetTile(Vector3 position)
    {
        int tileX = (int)((position.x + MapWidth / 2) / TileWidth);
        int tileY = (int)((position.y + MapHeight / 2) / TileHeight);
        return GetTile(tileX, tileY);
    }

    //获取鼠标下面的格子
    Tile GetTileUnderMouse()
    {
        Vector2 wordPos = GetWorldPosition();
        return GetTile(wordPos);
    }

    //获取鼠标所在位置的世界坐标
    Vector3 GetWorldPosition()
    {
        Vector3 viewPos = Camera.main.ScreenToViewportPoint(Input.mousePosition);
        Vector3 worldPos = Camera.main.ViewportToWorldPoint(viewPos);
        return worldPos;
    }
    #endregion
}

总结

1.请注意,本类对于格子和坐标的获取转换基于UI坐标恰好为(0,0)的前提条件下。

2.OnDrawGizmos函数块即使在未运行阶段也会运行,打包之后则会失效

另外,如果自己画的Gizmos要想在Game视图中能看到,需要把Game视图窗口右上角的"Gizmos"按钮点下去,否则尽管Scene能看到线,你在Game窗口就看不到绘制出来的格子了

编辑器扩展–MapEditor类

用途

使用基础Editor类的脚本为Map脚本加上控制UI,用来读取关卡和保存关卡。

代码

//用注解指定扩展的脚本对象
[CustomEditor(typeof(Map))]
public class MapEditor : Editor
{
    [HideInInspector]
    public Map Map = null;

    //关卡列表
    List<FileInfo> m_files = new List<FileInfo>();

    //当前编辑的关卡索引号
    int m_selectIndex = -1;
    
    //在编辑器里面绘制UI的官方回调函数
    public override void OnInspectorGUI()
    {
        //如果不使用这个方法,Map类原UI将会被覆盖,不会显示除了此回调函数之外的编辑器变量UI
        base.OnInspectorGUI();

        //只有运行的时候允许绘制UI
        if(Application.isPlaying)
        {
            //target获取当前关联的Mono脚本组件
            Map = target as Map;

            EditorGUILayout.BeginHorizontal();
            //关卡菜单下拉选择框
            int currentIndex = EditorGUILayout.Popup(m_selectIndex, GetNames(m_files));
            if (currentIndex != m_selectIndex)
            {
                m_selectIndex = currentIndex;

                //加载关卡
                LoadLevel();
            }
            if (GUILayout.Button("读取列表"))
            {
                //读取关卡列表
                LoadLevelFiles();
            }
            EditorGUILayout.EndHorizontal();

            EditorGUILayout.BeginHorizontal();
            if (GUILayout.Button("清除塔点"))
            {
                Map.ClearHolder();
            }
            if (GUILayout.Button("清除路径"))
            {
                Map.ClearRoad();
            }
            EditorGUILayout.EndHorizontal();

            if (GUILayout.Button("保存数据"))
            {
                //保存关卡
                SaveLevel();
            }
        }

        if (GUI.changed)
            EditorUtility.SetDirty(target);
    }

    void LoadLevelFiles()
    {
        //清除状态
        Clear();

        //加载列表
        m_files = Tools.GetLevelFiles();

        //默认加载第一个关卡
        if (m_files.Count > 0)
        {
            m_selectIndex = 0;
            LoadLevel();
        }
    }

    void LoadLevel()
    {
        FileInfo file = m_files[m_selectIndex];

        Level level = new Level();
        Tools.FillLevel(file.FullName, ref level);

        Map.LoadLevel(level);
    }

    void SaveLevel()
    {
        //获取当前加载的关卡
        Level level = Map.Level;
        //由于会重新修改level,我们得获取修改后的点位
        //临时索引点
        List<Point> list = null;
        
        //收集放塔点
        list = new List<Point>();
        for (int i = 0; i < Map.Grid.Count; i++)
        {
            Tile t = Map.Grid[i];
            if (t.CanHold)
            {
                Point p = new Point(t.X, t.Y);
                list.Add(p);
            }
        }
        level.Holder = list;

        //收集寻路点
        list = new List<Point>();
        for (int i = 0; i < Map.Road.Count; i++)
        {
            Tile t = Map.Road[i];
            Point p = new Point(t.X, t.Y);
            list.Add(p);
        }
        level.Path = list;

        //路径
        string fileName = m_files[m_selectIndex].FullName;

        //保存关卡
        Tools.SaveLevel(fileName, level);

        //弹框提示
        EditorUtility.DisplayDialog("保存关卡数据", "保存成功", "确定");
    }

    void Clear()
    {
        m_files.Clear();
        m_selectIndex = -1;
    }


    string[] GetNames(List<FileInfo> files)
    {
        List<string> names = new List<string>();
        foreach (FileInfo file in files)
        {
            names.Add(file.Name);
        }
        return names.ToArray();
    }
}

第4章: 游戏流程

前瞻

介绍

框架与编辑器已经构建完毕,本章节将正式遵循MVC框架开始游戏的主流程制作。

本游戏的用户端一共有5个场景(关卡编辑器是供开发者的单独场景),接下来我们会依次介绍。

结构图


蓝色为场景,黄色为对应场景下会触发的事件。

游戏初始化场景 (Game类)

场景介绍

本场景序号为场景0,默认只需要挂载Game脚本,连摄像机都不需要,本场景主要用来初始化和进入游戏循环。

用途

StaticData :用来初始化一些静态数据(不需要动态变化的,游戏写死的数据内容)

Game :用来初始化游戏,开启循环的类。由于会伴随游戏的全过程,我们让它提供一些常用的全局变量和方法供其他类随时调用

此外,本节的前置代码中还包含了参数类和事件(Ctronller)类。

代码

事件类

StartUpCommand (初始化装载事件,并且切换场景到开始游戏页面):

class StartUpCommand : Controller
{
    public override void Execute(object data)
    {
        //注册模型(Model)
        RegisterModel(new GameModel());
        RegisterModel(new RoundModel());

        //注册命令(Controller)
        RegisterController(Consts.E_EnterScene, typeof(EnterSceneComand));
        RegisterController(Consts.E_ExitScene, typeof(ExitSceneCommand));
        RegisterController(Consts.E_StartLevel, typeof(StartLevelCommand));
        RegisterController(Consts.E_EndLevel, typeof(EndLevelCommand));
        RegisterController(Consts.E_CountDownComplete, typeof(CountDownCompleteCommand));

        RegisterController(Consts.E_UpgradeTower, typeof(UpgradeTowerCommand));
        RegisterController(Consts.E_SellTower, typeof(SellTowerCommand));

        //初始化
        GameModel gModel = GetModel<GameModel>();
        gModel.Initialize();

    //进入开始界面  
Game.GetInstance().LoadScene(1);
    }
}
参数类

场景参数类SceneArgs:

public class SceneArgs
{
    //场景索引号
    public int SceneIndex;
}
静态数据类

静态数据类:

ublic class StaticData : Singleton<StaticData>
{
    Dictionary<int, LuoboInfo> m_Luobos = new Dictionary<int, LuoboInfo>();
    Dictionary<int, MonsterInfo> m_Monsters = new Dictionary<int, MonsterInfo>();
    Dictionary<int, TowerInfo> m_Towers = new Dictionary<int, TowerInfo>();
    Dictionary<int, BulletInfo> m_Bullets = new Dictionary<int, BulletInfo>();

    protected override void Awake()
    {
        base.Awake();
        InitLuobos();
        InitMonsters();
        InitTowers();
        InitBullets();
    }

  
    void InitLuobos()
    {
        m_Luobos.Add(0, new LuoboInfo() { ID = 0, Hp = 2 });
    }

    void InitMonsters()
    {
        m_Monsters.Add(0, new MonsterInfo() { ID = 0, Hp = 5, MoveSpeed = 1f, Price = 1 });
        m_Monsters.Add(1, new MonsterInfo() { ID = 1, Hp = 5, MoveSpeed = 1f, Price = 2 });
        m_Monsters.Add(2, new MonsterInfo() { ID = 2, Hp = 15, MoveSpeed = 2f, Price = 5 });
        m_Monsters.Add(3, new MonsterInfo() { ID = 3, Hp = 20, MoveSpeed = 2f, Price = 10 });
        m_Monsters.Add(4, new MonsterInfo() { ID = 4, Hp = 20, MoveSpeed = 2f, Price = 15 });
        m_Monsters.Add(5, new MonsterInfo() { ID = 5, Hp = 100, MoveSpeed = 0.5f, Price = 20 });
    }

    void InitTowers()
    {
        m_Towers.Add(0, new TowerInfo() { ID = 0, PrefabName = "Bottle", NormalIcon = "Bottle/Bottle01", DisabledIcon = "Bottle/Bottle00", MaxLevel = 3, BasePrice = 1, ShotRate = 2,    GuardRange = 3f, UseBulletID = 0 });
        m_Towers.Add(1, new TowerInfo() { ID = 1, PrefabName = "Fan",    NormalIcon = "Fan/Fan01",       DisabledIcon = "Fan/Fan00",       MaxLevel = 3, BasePrice = 2, ShotRate = 0.3f, GuardRange = 3f, UseBulletID = 1 });
    }

    void InitBullets()
    {
        m_Bullets.Add(0, new BulletInfo() { ID = 0, PrefabName = "BallBullet", BaseSpeed = 5f, BaseAttack = 1 });
        m_Bullets.Add(1, new BulletInfo() { ID = 1, PrefabName = "FanBullet", BaseSpeed = 2f, BaseAttack = 1 });
    }

    public LuoboInfo GetLuoboInfo()
    {
        return m_Luobos[0];
    }

    public MonsterInfo GetMonsterInfo(int monsterID)
    {
        return m_Monsters[monsterID];
    }

    public TowerInfo GetTowerInfo(int towerID)
    {
        return m_Towers[towerID];
    }

    public BulletInfo GetBulletInfo(int bulletID)
    {
        return m_Bullets[bulletID];
    }
}
初始化类Game

游戏初始化类Game :

[RequireComponent(typeof(ObjectPool))]
[RequireComponent(typeof(Sound))]
[RequireComponent(typeof(StaticData))]
public class Game : ApplicationBase<Game>
{
    //全局访问功能
    [HideInInspector] public ObjectPool ObjectPool = null; //对象池
    [HideInInspector] public Sound Sound = null;//声音控制
    [HideInInspector] public StaticData StaticData = null;//静态数据

    //全局方法
    public void LoadScene(int level)
    {
        //---退出旧场景
        //事件参数
        SceneArgs e = new SceneArgs() 
        { 
            SceneIndex = SceneManager.GetActiveScene().buildIndex 
        };

        //发布事件
        SendEvent(Consts.E_ExitScene, e);

        //---加载新场景
        SceneManager.LoadScene(level, LoadSceneMode.Single);
    }

    void OnLevelWasLoaded(int level)
    {
        Debug.Log("OnLevelWasLoaded:" + level);

        //事件参数
        SceneArgs e = new SceneArgs() { SceneIndex = level };

        //发布事件
        SendEvent(Consts.E_EnterScene, e);
    }

    //游戏入口
    void Start()
    {
        //确保Game对象一直存在
        Object.DontDestroyOnLoad(this.gameObject);

        //全局单例赋值
        ObjectPool = ObjectPool.Instance;
        Sound = Sound.Instance;
        StaticData = StaticData.Instance;

        //注册启动命令
        RegisterController(Consts.E_StartUp, typeof(StartUpCommand));

        //启动游戏
        SendEvent(Consts.E_StartUp);
    }
}

总结

1.由于触发事件固定传入一个object,这里我们为每个需要传入的参数创建一个类并放在一个文件夹中,方便我们对所有事件参数进行统一管理。

2.OnLevelWasLoaded函数是类似事件函数的Unity官方回调函数,会在你切换场景的时候自动触发,场上无论有多少个存在此回调函数的脚本都会在切换场景依次进行触发。

3.通过注解RequireComponent,可以初始化的时候在物体上挂载自动挂载其他管理脚本。方便管理。

4.本文在New事件参数对象的时候,采取了手动为函数对象里面的值赋值的行为,你可以理解为在外部实现构造函数。

开始界面(UIStart类)

场景介绍


本场景序号为场景1开始界面

用途

会采用itween实现一些基本的动画效果(云和飞行的怪物),并且提供按钮供玩家开始(本文只完成冒险模式,其他两个按钮是摆设)

代码

UI动画

Cloud脚本 (云):

public class Cloud : MonoBehaviour
{
    public float OffsetX = 1000; //X方向的偏移量
    public float Duration = 1f;//周期时间

    void Start()
    {
        iTween.MoveBy(gameObject, iTween.Hash(
            "x", OffsetX,
            "easeType", iTween.EaseType.linear,
            "loopType", iTween.LoopType.loop,
            "time", Duration));
    }
}

Bird脚本(黄色飞行怪物):

public class Bird : MonoBehaviour 
{
    public float Time = 1; //一次循环所需时间
    public float OffsetY = 30; //Y方向浮动偏移

	void Start () 
    {
        iTween.MoveBy(this.gameObject, iTween.Hash(
            "y", OffsetY,
            "easeType", iTween.EaseType.easeInOutSine,
            "loopType", iTween.LoopType.pingPong,
            "time", Time));
	}
}
UIStart类(View层)

UIStart:

public class UIStart : View
{
    public override string Name
    {
        get { return Consts.V_Start; }
    }

//会被面板手动挂载在冒险模式Button上
    public void GotoSelect()
    {
        Game.GetInstance().LoadScene(2);
    }

    public override void HandleEvent(string eventName, object data)
    {
        
    }
}

总结

选关界面(UISelect类 )