到目前为止我们实现的功能有
功能
规则
实现
最佳分数
存储并记录最佳分数
√
倒计时
游戏倒计时10秒
计分
点错一次扣5分,点对一次得10分,结束时倒计时每剩余1秒得10分
√
金币
每次点击正确的方块,有一定概率获得1~3金币
商店
游戏开始前可以在商店购买,能够抵消点击错误的次数
成就
百分成就、手残成就(分数为负数)、零失误成就、
重开
游戏结束后回到首页再来一次
在这一节我们实现所有的功能,
纸上设计 首先整理思路,设计草图
然后可以做一个总览图
一般在交流沟通的时候,Command和Event都不会表示
针对每一个功能交互会单独画一张图,各个层级之间有连线,就像前两节中的那样
框架修改 BindableProperty引入注销机制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 using System;namespace FrameWorkDesign { public class BindableProperty <T > where T : IEquatable <T > { private T m_Value = default ; public T Value { get => m_Value; set { if (!value .Equals(m_Value)) { m_Value = value ; m_OnValueChanged?.Invoke(value ); } } } private Action<T> m_OnValueChanged = v => { }; public IUnregister RegisterOnValueChanged (Action<T> onValueChanged ) { m_OnValueChanged += onValueChanged; return new BindablePropertyUnregister<T>() { BindableProperty = this , OnValueChanged = onValueChanged }; } public void UnRegisterOnValueChanged (Action<T> onValueChanged ) { m_OnValueChanged -= onValueChanged; } public class BindablePropertyUnregister <T > : IUnregister where T : IEquatable <T > { public BindableProperty<T> BindableProperty { get ; set ; } public Action<T> OnValueChanged { get ; set ; } public void Unregister () { BindableProperty.UnRegisterOnValueChanged(OnValueChanged); BindableProperty = null ; OnValueChanged = null ; } } } }
这里我们模仿TypeEventSystem,给BindableProperty添加了注销机制,由于BindableProperty本身是泛型类,所以不用前者那么麻烦
删除Architecture中无用的方法 我们在之前重构Architecture之后,里面有一些单例化时声明的静态方法都没用了,我们可以删掉
在Architecture
脚本中,删掉下面的代码
1 2 3 4 5 6 7 8 9 10 11 12 public static T Get <T >() where T : class { MakeSureArchitecture(); return m_Architecture.m_Container.Get<T>(); } public static void Register <T >(T instance ){ MakeSureArchitecture(); m_Architecture.m_Container.Register<T>(instance); }
实现 接下来的文章顺序并不是开发顺序,而是根据上面图表的排列顺序贴的代码
添加事件 在FrameworkDesign——Example——Scripts——Event文件夹内新建OnCountDownEndEvent
1 2 3 4 5 6 7 namespace FrameWorkDesign.Example { public class OnCountDownEndEvent { } }
修改GameModel 修改GameModel
,我们要为其添加一个Life属性,顺便修改其中报错的地方,也就是更换BestScore的监听方法,同时给Life属性和Gold属性添加监听
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 namespace FrameWorkDesign.Example { public interface IGameModel : IModel { BindableProperty<int > Life { get ; } } public class GameModel : AbstractModel , IGameModel { public BindableProperty<int > Life { get ; } = new BindableProperty<int >() { Value = 3 }; protected override void OnInit () { var storage = this .GetUtility<IStorage>(); BestScore.Value = storage.LoadInt(nameof (BestScore)); BestScore.RegisterOnValueChanged(score => storage.SaveInt(nameof (BestScore), score)); Life.Value = storage.LoadInt(nameof (Life), 3 ); Life.RegisterOnValueChanged(lifenum => storage.SaveInt(nameof (Life), lifenum)); Gold.Value = storage.LoadInt(nameof (Gold), 3 ); Gold.RegisterOnValueChanged(goldnum => storage.SaveInt(nameof (Gold), goldnum)); } } }
添加CountDown系统 在FrameworkDesign——Example——Scripts——System文件夹内新建ICountDownSystem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 using System;namespace FrameWorkDesign.Example { public interface ICountDownSystem : ISystem { int CurrentRemainSeconds { get ; } void Update () ; } public class CountDownSystem : AbstractSystem ,ICountDownSystem { private DateTime m_GameStartTime { get ; set ; } private bool m_Started = false ; public int CurrentRemainSeconds => 10 - (int )(DateTime.Now - m_GameStartTime).TotalSeconds; protected override void OnInit () { this .RegisterEvent<GameStartEvent>(e => { m_Started = true ; m_GameStartTime = DateTime.Now; }); this .RegisterEvent<GameEndEvent>(e => { m_Started = false ; }); } public void Update () { if (m_Started) { if (DateTime.Now - m_GameStartTime > TimeSpan.FromSeconds(10 )) { this .SendEvent<OnCountDownEndEvent>(); m_Started = false ; } } } } }
添加Achievement系统 我们在在FrameworkDesign——Example——Scripts——System文件夹内新建IAchievementSystem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 using UnityEngine;using System;using System.Collections.Generic;using System.Linq;using System.Threading.Tasks;namespace FrameWorkDesign.Example { public interface IAchievementSystem : ISystem { } public class AchievementItem { public string Name { get ; set ; } public Func<bool > CheckComplete { get ; set ; } public bool Unlocked { get ; set ; } } public class AchievementSystem : AbstractSystem , IAchievementSystem { private List<AchievementItem> m_AchievementItems = new List<AchievementItem>(); private bool m_Missed = false ; protected override void OnInit () { this .RegisterEvent<OnMissEvent>(e => { m_Missed = true ; }); this .RegisterEvent<GameStartEvent>(e => { m_Missed = false ; }); m_AchievementItems.Add(new AchievementItem() { Name = "百分成就" , CheckComplete = () => this .GetModel<IGameModel>().BestScore.Value > 100 }); m_AchievementItems.Add(new AchievementItem() { Name = "手残" , CheckComplete = () => this .GetModel<IGameModel>().Score.Value < 0 }); m_AchievementItems.Add(new AchievementItem() { Name = "零失误成就" , CheckComplete = () => !m_Missed }); m_AchievementItems.Add(new AchievementItem() { Name = "全成就" , CheckComplete = () => m_AchievementItems.Count(item => item.Unlocked) >= 3 }); this .RegisterEvent<GameEndEvent>(async e => { await Task.Delay(TimeSpan.FromSeconds(0.1f )); foreach (var achievementItem in m_AchievementItems) { if (!achievementItem.Unlocked && achievementItem.CheckComplete()) { achievementItem.Unlocked = true ; Debug.Log("解锁成就:" + achievementItem.Name); } } }); } } }
修改Score系统 修改IScoreSystem
,让它获取CountDown系统,并把倒计时的分数给加上,同时删掉之前的Debug,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 using UnityEngine;namespace FrameWorkDesign.Example { public interface IScoreSystem : ISystem { } public class ScoreSystem : AbstractSystem , IScoreSystem { protected override void OnInit () { var gameModel = this .GetModel<IGameModel>(); this .RegisterEvent<GameEndEvent>(e => { var countDownSystem = this .GetSystem<ICountDownSystem>(); var timeScore = countDownSystem.CurrentRemainSeconds * 10 ; gameModel.Score.Value += timeScore; if (gameModel.Score.Value > gameModel.BestScore.Value) { gameModel.BestScore.Value = gameModel.Score.Value; Debug.Log("新纪录" ); } }); this .RegisterEvent<OnEnemyKillEvent>(e => { gameModel.Score.Value += 10 ; Debug.Log("得10分" ); Debug.Log("当前分数:" + gameModel.Score.Value); }); this .RegisterEvent<OnMissEvent>(e => { gameModel.Score.Value -= 5 ; Debug.Log("减5分" ); Debug.Log("当前分数:" + gameModel.Score.Value); }); } } }
将新添加的系统从PointGame中注册 修改PointGame
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 namespace FrameWorkDesign.Example { public class PointGame : Architecture <PointGame > { protected override void Init () { RegisterSystem<IScoreSystem>(new ScoreSystem()); RegisterSystem<ICountDownSystem>(new CountDownSystem()); RegisterSystem<IAchievementSystem>(new AchievementSystem()); RegisterModel<IGameModel>(new GameModel()); RegisterUtility<IStorage>(new PlayerPrefStorage()); } } }
添加BuyLifeCommand 我们在FrameworkDesign——Example——Scripts——Command文件夹内新建BuyLifeCommand
1 2 3 4 5 6 7 8 9 10 11 12 namespace FrameWorkDesign.Example { public class BuyLifeCommand : AbstractCommand { protected override void OnExecute () { var gameModel = this .GetModel<IGameModel>(); gameModel.Gold.Value--; gameModel.Life.Value++; } } }
修改KillEnemyCommand 我们修改KillEnemyCommand
,实现点击时有30%概率随机获得1~3金币
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 using UnityEngine;namespace FrameWorkDesign.Example { public class KillEnemyCommand : AbstractCommand { protected override void OnExecute () { var gameModel = this .GetModel<IGameModel>(); gameModel.KillCount.Value++; if (Random.Range(0 , 10 ) < 3 ) gameModel.Gold.Value += Random.Range(1 , 3 ); this .SendEvent<OnEnemyKillEvent>(); if (gameModel.KillCount.Value == 9 ) { this .SendEvent<GameEndEvent>(); } } } }
修改MissCommand 我们修改MissCommand
,当Life的值不为0时,不发送OnMissEvent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 namespace FrameWorkDesign.Example { public class MissCommand : AbstractCommand { protected override void OnExecute () { var gameModel = this .GetModel<IGameModel>(); if (gameModel.Life.Value > 0 ) { gameModel.Life.Value--; } else { this .SendEvent<OnMissEvent>(); } } } }
修改StartGameCommand GameModel中的KillCount和Score是不需要储存的,我们在游戏运行时如果选择重来一次,内存中的这两个值应该重置
1 2 3 4 5 6 7 8 9 10 11 12 13 namespace FrameWorkDesign.Example { public class StartGameCommand : AbstractCommand { protected override void OnExecute () { var gameModel = this .GetModel<IGameModel>(); gameModel.Score.Value = 0 ; gameModel.KillCount.Value = 0 ; this .SendEvent<GameStartEvent>(); } } }
拼开始菜单UI界面 直接展示拼好的样子
为了方便,我们在Hierarchy中这么排列
修改GameStartPanel
里面的代码,购买生命属于修改底层数据,我们发送Command,而金币和生命值属于直接展现,所以添加监听
每当我们在框架中需要注销一些方法时,一定要记住将当前类的一些引用置空
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 using UnityEngine;using UnityEngine.UI;namespace FrameWorkDesign.Example { public class GameStartPanel : MonoBehaviour ,IController { private IGameModel m_GameModel; IArchitecture IBelongToArchitecture.GetArchitecture() { return PointGame.Interface; } void Start () { transform.Find("Button_Start" ).GetComponent<Button>() .onClick.AddListener(() => { gameObject.SetActive(false ); this .SendCommand<StartGameCommand>(); }); transform.Find("Button_Buylife" ).GetComponent<Button>() .onClick.AddListener(() => { this .SendCommand<BuyLifeCommand>(); }); m_GameModel = this .GetModel<IGameModel>(); m_GameModel.Gold.RegisterOnValueChanged(OnGoldValueChanged); m_GameModel.Life.RegisterOnValueChanged(OnLifeValueChanged); OnGoldValueChanged(m_GameModel.Gold.Value); OnLifeValueChanged(m_GameModel.Life.Value); transform.Find("Text_BestScore" ).GetComponent<Text>().text = "最高分:" + m_GameModel.BestScore.Value; } private void OnLifeValueChanged (int life ) { transform.Find("Text_Life" ).GetComponent<Text>().text = "生命:" + life; } private void OnGoldValueChanged (int gold ) { if (gold > 0 ) { transform.Find("Button_Buylife" ).GetComponent<Button>().interactable = true ; } else { transform.Find("Button_Buylife" ).GetComponent<Button>().interactable = false ; } transform.Find("Text_Gold" ).GetComponent<Text>().text = "金币:" + gold; } private void OnDestroy () { m_GameModel.Gold.UnRegisterOnValueChanged(OnGoldValueChanged); m_GameModel.Life.UnRegisterOnValueChanged(OnLifeValueChanged); m_GameModel = null ; } } }
在编译错误改好之前,下面的脚本都不可以挂载 拼游戏中UI界面 我们在Hierarchy窗口中UI——Canvas新建Panel_InGame,然后拼接游戏中UI,注意要删除掉Panel_InGame的Image组件
在FrameworkDesign——Example——Scripts——UI内部新建GamingPanel
,该监听的添加监听
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 using UnityEngine;using UnityEngine.UI;namespace FrameWorkDesign.Example { public class GamingPanel : MonoBehaviour ,IController { private ICountDownSystem m_CountDownSystem; private IGameModel m_GameModel; IArchitecture IBelongToArchitecture.GetArchitecture() { return PointGame.Interface; } private void Awake () { m_CountDownSystem = this .GetSystem<ICountDownSystem>(); m_GameModel = this .GetModel<IGameModel>(); m_GameModel.Gold.RegisterOnValueChanged(OnGoldValueChanged); m_GameModel.Life.RegisterOnValueChanged(OnLifeValueChanged); m_GameModel.Score.RegisterOnValueChanged(OnScoreValueChanged); OnGoldValueChanged(m_GameModel.Gold.Value); OnLifeValueChanged(m_GameModel.Life.Value); OnScoreValueChanged(m_GameModel.Score.Value); } private void OnLifeValueChanged (int life ) { transform.Find("Text_Life" ).GetComponent<Text>().text = "生命:" + life; } private void OnGoldValueChanged (int gold ) { transform.Find("Text_Gold" ).GetComponent<Text>().text = "金币:" + gold; } private void OnScoreValueChanged (int score ) { transform.Find("Text_Score" ).GetComponent<Text>().text = "分数:" + score; } void Update () { if (Time.frameCount % 20 == 0 ) { transform.Find("Text_CountDown" ).GetComponent<Text>().text = m_CountDownSystem.CurrentRemainSeconds + "s" ; m_CountDownSystem.Update(); } } private void OnDestroy () { m_GameModel.Gold.UnRegisterOnValueChanged(OnGoldValueChanged); m_GameModel.Life.UnRegisterOnValueChanged(OnLifeValueChanged); m_GameModel.Score.UnRegisterOnValueChanged(OnGoldValueChanged); m_GameModel = null ; m_CountDownSystem = null ; } } }
拼游戏通关UI界面 我们在Hierarchy中的Panel_GamePass添加一些UI元素
在FrameworkDesign——Example——Scripts——UI内部新建GamePassPanel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 using UnityEngine;using UnityEngine.UI;namespace FrameWorkDesign.Example { public class GamePassPanel : MonoBehaviour ,IController { IArchitecture IBelongToArchitecture.GetArchitecture() { return PointGame.Interface; } void OnEnable () { transform.Find("Text_RemainTime" ).GetComponent<Text>().text = "剩余时间:" + this .GetSystem<ICountDownSystem>().CurrentRemainSeconds + "s" ; var gameModel = this .GetModel<IGameModel>(); transform.Find("Text_BestScore" ).GetComponent<Text>().text = "最高分:" + gameModel.BestScore.Value; transform.Find("Text_Score" ).GetComponent<Text>().text = "得分:" + gameModel.Score.Value; } } }
拼游戏结束UI界面
游戏通关界面的按钮和游戏结束界面的按钮我们都没有添加监听,这里我们添加上
Panel_GamePass的Button监听
Panel_GameEnd的Button监听
Panel_GameStart的Button_Start监听
因为它们都是表现层,所以我们这里都直接在inspector里面设置
修改UIRoot 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 using UnityEngine;namespace FrameWorkDesign.Example { public class UIRoot : MonoBehaviour ,IController { void Start () { this .RegisterEvent<GameEndEvent>(OnGameEnd); this .RegisterEvent<OnCountDownEndEvent>(e => { transform.Find("Canvas/Panel_InGame" ).gameObject.SetActive(false ); transform.Find("Canvas/Panel_GameEnd" ).gameObject.SetActive(true ); }).UnregisterWhenGameObjectDestroyed(gameObject); } private void OnGameEnd (GameEndEvent e ) { transform.Find("Canvas/Panel_InGame" ).gameObject.SetActive(false ); transform.Find("Canvas/Panel_GamePass" ).gameObject.SetActive(true ); } void OnDestroy () { this .UnregisterEvent<GameEndEvent>(OnGameEnd); } IArchitecture IBelongToArchitecture.GetArchitecture() { return PointGame.Interface; } } }
注意这里的GameEndEvent
触发的是游戏通关界面,而不是游戏结束界面
修改GameRoot 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 using UnityEngine;namespace FrameWorkDesign.Example { public class GameRoot : MonoBehaviour ,IController { private void Start () { this .RegisterEvent<GameStartEvent>(OnGameStart); this .RegisterEvent<OnCountDownEndEvent>(e => { transform.Find("Enemies" ).gameObject.SetActive(false ); }).UnregisterWhenGameObjectDestroyed(gameObject); this .RegisterEvent<GameEndEvent>(e => { transform.Find("Enemies" ).gameObject.SetActive(false ); }).UnregisterWhenGameObjectDestroyed(gameObject); } private void OnGameStart (GameStartEvent e ) { var enemyRoot = transform.Find("Enemies" ); enemyRoot.gameObject.SetActive(true ); foreach (Transform childTrans in enemyRoot) { childTrans.gameObject.SetActive(true ); } } void OnDestroy () { this .UnregisterEvent<GameStartEvent>(OnGameStart); } IArchitecture IBelongToArchitecture.GetArchitecture() { return PointGame.Interface; } } }
修改Enemy 因为游戏要重新开始,所以不能使用Destroy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 using UnityEngine;namespace FrameWorkDesign.Example { public class Enemy : MonoBehaviour ,IController { IArchitecture IBelongToArchitecture.GetArchitecture() { return PointGame.Interface; } private void OnMouseDown () { gameObject.SetActive(false ); this .SendCommand<KillEnemyCommand>(); } } }
修改编译错误 大部分错误是来自《CounterApp》内的
修改CounterViewController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 using UnityEngine;using UnityEngine.UI;using FrameWorkDesign;namespace CounterApp { public class CounterViewController : MonoBehaviour , IController { private void Start () { m_CounterModel.Count.RegisterOnValueChanged(UpdateView); } private void OnDestroy () { m_CounterModel.Count.UnRegisterOnValueChanged(UpdateView); } } public class CounterModel : AbstractModel ,ICounterModel { protected override void OnInit () { Count.RegisterOnValueChanged( count => { storage.SaveInt("COUNTER_COUNT" , count); }); } } }
修改《CounterApp》的AchievementSystem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 using UnityEngine;using FrameWorkDesign;namespace CounterApp { public class AchievementSystem : AbstractSystem , IAchievementSystem { protected override void OnInit () { counterModel.Count.RegisterOnValueChanged(newCount => { if (previousCount<10 && newCount >= 10 ) { Debug.Log("点击10次" ); }else if (previousCount < 20 && newCount >= 20 ) { Debug.Log("点击20次" ); } previousCount = newCount; }); } } }
接下来挂载好UI的脚本,就能够测试了
功能
规则
实现
最佳分数
存储并记录最佳分数
√
倒计时
游戏倒计时10秒
√
计分
点错一次扣5分,点对一次得10分,结束时倒计时每剩余1秒得10分
√
金币
每次点击正确的方块,有一定概率获得1~3金币
√
商店
游戏开始前可以在商店购买,能够抵消点击错误的次数
√
成就
百分成就、手残成就(分数为负数)、零失误成就、
√
重开
游戏结束后回到首页再来一次
√