到目前为止我们实现的功能有

功能 规则 实现
最佳分数 存储并记录最佳分数
倒计时 游戏倒计时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()//通过监听事件来改变System本身的状态
{
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
});

//成就系统一般需要持久化数据,如果需要持久化也是在这里进行,可以让Unlock变成BindableProperty
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界面

直接展示拼好的样子

UI界面

为了方便,我们在Hierarchy中这么排列

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组件

游戏中UI

Hierarchy结构

在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;
}


// Update is called once per frame
void Update()
{
if (Time.frameCount % 20 == 0)//每20帧更新一次
{
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元素

通关UI

Hierarchy元素排列

在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()//谨慎使用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界面

游戏结束UI

Hierarchy顺序

游戏通关界面的按钮和游戏结束界面的按钮我们都没有添加监听,这里我们添加上

Panel_GamePass的Button监听

Panel_GamePass的Button

Panel_GameEnd的Button监听

Panel_GameEnd的Button

Panel_GameStart的Button_Start监听

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);//添加关闭Panel_InGame的监听
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);
//Transfrom属性自己实现了枚举,所以直接使用Foreach就可以遍历子对象
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金币
商店 游戏开始前可以在商店购买,能够抵消点击错误的次数
成就 百分成就、手残成就(分数为负数)、零失误成就、
重开 游戏结束后回到首页再来一次