Week06-GameObject Factory Frisbee

游戏对象工厂:打飞碟游戏

这篇博客会介绍一个打飞碟游戏,主要是引入一个游戏对象的创建与回收机制。

如果每次需要一个新的游戏对象的时候都重新创建它,用完了又马上销毁它,会造成资源的浪费以及游戏性能的下降。本次作业中的飞碟就是一个很好的例子,需要在游戏过程中源源不断地飞出,之后又被击落或者飞出屏幕。

因此,首先创建飞碟工厂的类:在游戏中,这个工厂需要根据用户(游戏整体)的需求借出飞碟,当用户不再使用的时候就由工厂将飞碟回收。如果用户需要的时候工厂也没有飞碟了,工厂就再去生产(加载新的预制)。

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
public class DiskFactory : MonoBehaviour {
public GameObject disk;
//存放已借出飞碟
private List<DiskData> used = new List<DiskData>();
//存放可借出飞碟
private List<DiskData> free = new List<DiskData>();

private void Awake() {
//加载预制
disk = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/UFO"), Vector3.zero, Quaternion.identity);
disk.AddComponent<DiskData>();
disk.SetActive(false);
}

public GameObject getDisk(int gameRound) {
//注意:飞碟出厂的时候active为false,外界扔了它才能active
GameObject newDisk = null;
//如果还有飞碟可以借出
if(free.Count > 0) {
//借出第一个飞碟
newDisk = free[0].gameObject;
//借出飞碟不再在可借出链表中
free.Remove(free[0]);
}
//如果没有飞碟可借出
else {
//从预制重新实例化一个飞碟
newDisk = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/UFO"), Vector3.zero, Quaternion.identity);
newDisk.SetActive(false);
newDisk.AddComponent<DiskData>();//挂载记录飞碟属性的DiskData类
}

/**游戏规则:这里根据自己的想法设定
*每个飞碟的初速度在一定范围内随机大小和方向,每轮均出现红黄蓝三种颜色、大中小三种大小的飞碟
*随回合的增加飞碟速度回变快,扔出飞碟的间隔也会缩小
*/

//随机飞碟颜色
int colorNo = Random.Range(0, 3)%3;
switch(colorNo) {
case 0:
newDisk.GetComponent<DiskData>().color = Color.yellow;
newDisk.GetComponent<Renderer>().material.color = Color.yellow;
break;
case 1:
newDisk.GetComponent<DiskData>().color = Color.red;
newDisk.GetComponent<Renderer>().material.color = Color.red;
break;
case 2:
newDisk.GetComponent<DiskData>().color = Color.blue;
newDisk.GetComponent<Renderer>().material.color = Color.blue;
break;
}

//随机飞碟大小
int sizeNo = Random.Range(0, 3)%3;
switch(sizeNo) {
case 1:
newDisk.transform.localScale = new Vector3(1.2f,0.05f,1.2f);
newDisk.GetComponent<DiskData>().size = 1;
break;
case 2:
newDisk.transform.localScale = new Vector3(2.4f,0.1f,2.4f);
newDisk.GetComponent<DiskData>().size = 2;
break;
case 0:
newDisk.transform.localScale = new Vector3(3.6f,0.15f,3.6f);
newDisk.GetComponent<DiskData>().size = 3;
break;
}

//随机飞碟的初速度
float tempRand = Random.Range(-1f,1f);
int posOrNeg;
if(tempRand > 0) {
posOrNeg = 1;
}
else {
posOrNeg = -1;
}
newDisk.GetComponent<DiskData>().xSpeed = posOrNeg * Random.Range( 1.5f*(gameRound -1), 1.5f*gameRound);
tempRand = Random.Range(-1f,1f);
if(tempRand > 0) {
posOrNeg = 1;
}
else {
posOrNeg = -1;
}
newDisk.GetComponent<DiskData>().ySpeed = posOrNeg * Random.Range(2f + (gameRound-1) * 0.5f, 2f + gameRound * 0.5f);

used.Add(newDisk.GetComponent<DiskData>());
newDisk.name = newDisk.GetInstanceID().ToString();
return newDisk;
}

public void FreeDisk(GameObject disk) {
//用于在工厂中标记disk所对应的used飞碟
DiskData temp = null;
//遍历被用过的飞碟
foreach (DiskData i in used) {
//从飞碟工厂生产出飞碟开始,ID就一直不变,不管借出还是回收,永远是那固定的几个
if(i.gameObject.GetInstanceID() == disk.GetInstanceID()) {
temp = i;
}
}
if(temp != null) {//else在已借出链表中不存在,说明已经在可借出free链表了,就不用管
//放到free链表
temp.gameObject.SetActive(false);
used.Remove(temp);
free.Add(temp);
}
}
}

上文注意到游戏工厂中的飞碟添加了DiskData组件:作用是可以使飞碟工厂中为飞碟设置的种种属性一直挂在飞碟上。

1
2
3
4
5
6
public class DiskData : MonoBehaviour {
public int size;
public Color color;
public float xSpeed;
public float ySpeed;
}

目前仅仅完成了飞碟属性的设定,飞碟工厂生产回收飞碟的步骤,但至少还要让飞碟正确地飞入场景中。这就需要用上节课所学的动作管理器。(基类SSAction的代码和PPT上一致,就不放到这里了)

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
public class CCFlyAction : SSAction {
public float g;
public float xSpeed;
public float ySpeed;
public float timeCount;
Vector3 direction;
public override void Start () {
g = 5f;
timeCount = 0;
enable = true;
destroy = false;
//这里就体现出了将飞碟的属性作为组件挂在到飞碟上的重要性,在其他地方不经过飞碟工厂就可以很方便获取飞碟属性
xSpeed = gameobject.GetComponent<DiskData>().xSpeed;
ySpeed = gameobject.GetComponent<DiskData>().ySpeed;
}
public override void Update () {
if(gameobject.activeSelf) {
timeCount += Time.deltaTime;
gameobject.transform.Translate(new Vector3(xSpeed*Time.deltaTime, (ySpeed - timeCount*g)*Time.deltaTime, 0));

//飞碟坠毁:这里具体的数值设置需要根据摄像机的位置以及视口大小来调节
if(gameobject.transform.position.y < -10) {
this.destroy = true;
this.enable = false;
this.callback.SSActionEvent(this);
}
}
}
public static CCFlyAction getSSAction() {
CCFlyAction action = ScriptableObject.CreateInstance<CCFlyAction>();
return action;
}
}

当飞碟的动作设置完毕之后,需要对飞碟动作进行统一管理,这时候也和上节课内容一样,需要动作管理器:这里与最开始设置游戏对象工厂原理相似,也设置一个动作工厂,飞碟通过调用GetSSAction获得动作。其基类和接口的实现也与PPT一致。

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
public class CCActionManager : SSActionManager, ISSActionCallback {
public FirstController sceneController;
protected new void Start() {
sceneController = (FirstController)Director.getInstance().currentSceneController;
sceneController.actionManager = this;
}
public int numberOfDisks;

private List<SSAction> used = new List<SSAction>();
private List<SSAction> free = new List<SSAction>();

//从工厂中获得动作
SSAction GetSSAction() {
SSAction action = null;
if(free.Count > 0) {
action = free[0];
free.Remove(free[0]);
}
else {
action = ScriptableObject.Instantiate<CCFlyAction>(CCFlyAction.getSSAction());
}
used.Add(action);
return action;
}
//回收used中的动作,重新放入free
public void FreeSSAction (SSAction action) {
SSAction temp = null;
foreach(SSAction i in used) {
if(i.gameobject.GetInstanceID() == action.GetInstanceID()) {
temp = i;
}
}
if(temp != null) {
temp.reset();
free.Add(temp);
used.Remove(temp);
}
}
/**
*游戏开始时,
*1. 将飞碟队列中的每一个飞碟从动作工厂中拿到一个动作
*2. 用SSActionManager中RunAction让每个动作得以执行同时调用callback
*/
public void startGame(Queue <GameObject> diskQueue) {
Debug.Log("start game");
foreach (GameObject temp in diskQueue) {
RunAction(temp, GetSSAction(), this);
}
}
// implment interface
/**
*当一个飞碟飞起落下的全部动作完成之后
*1. 需要做的动作总数减少
*2. 飞碟工厂回收一个飞碟
*3. 动作管理者回收一个动作
*/
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0,
string strParam = null,
Object objectParam = null) {
if(source is CCFlyAction) {
numberOfDisks--;
DiskFactory df = Singleton<DiskFactory>.Instance;
df.FreeDisk(source.gameobject); //可以用这种方式获取了被挂在了特定component的游戏对象
FreeSSAction(source);
}
}
}

飞碟动作控制器设置好之后,需要将其与游戏场景连接,即在合适的时机让飞碟飞入,在FirstController中实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
*随机扔出飞碟的位置
*/
void randomThrow() {
if(disks.Count != 0) {
//得到队头飞碟
GameObject disk = disks.Dequeue();
Vector3 randPos = Vector3.zero;
//根据轮数设置难度:即每轮飞碟出现位置的范围不同
float x = Random.Range(-0.5f * gameRound - 3f, 0.5f * gameRound + 3f);
float y = Random.Range( - 0.1f * gameRound, 0.8f * gameRound);
randPos = new Vector3(x, y, 0);
disk.transform.position = randPos;
//激活飞碟
disk.SetActive(true);
}
}

场景中飞碟飞入飞出的动作设置完毕后,还需要对用户的行为进行相应。先用射线完成鼠标打飞碟的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//用户行为接口
public interface IUserAction {
//游戏状态相关
void getGameStart();
void getGameOver();
int getGameState();
void setGameState(int gameState);
//获取得分
int getScore();
//获取游戏轮数
int getRound();
//打飞碟
void hit(Vector3 pos);
}

FirstController中对void hit(Vector3 pos)的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
public void hit(Vector3 pos) {
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
for(int i = 0; i < hits.Length; i++) {
RaycastHit hit = hits[i];
if(hit.collider.gameObject.GetComponent<DiskData>() != null) {
scoreRecorder.Record(hit.collider.gameObject);
hit.collider.gameObject.transform.position = new Vector3(0,-20,0);
}
}
}

再在UserGUI中检测鼠标点击事件,调用hit

1
2
3
4
5
6
void Update () {
if (Input.GetButtonDown ("Fire1")) {
Vector3 mp = Input.mousePosition;
action.hit(mp);
}
}

把基础的游戏对象工厂、动作管理器实现好了之后,就可以用FirstController整合起来:

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 System.Collections;
using System.Collections.Generic;
using UnityEngine;
using SceneBasicCodes;
using ActionBasicCodes;

public class FirstController : MonoBehaviour, IUserAction, ISceneController {
public CCActionManager actionManager;
public ScoreRecorder scoreRecorder {get; set;}
private Queue<GameObject> disks = new Queue<GameObject> ();
private int gameRound = 0;
private int numberOfDisks; //每轮有多少个飞碟
public float throwPeriod = 1; //间隔多少秒出现一个飞碟
public int totalRound = 10;//一共有多少轮
private float timeInterval = 0;
private int gameState = 1;//0->not in game, 1->in game
private UserGUI userGUI;
// Use this for initialization
void Awake () {
//单例模式
Director director = Director.getInstance();
director.currentSceneController = this;
numberOfDisks = 10;
this.gameObject.AddComponent<ScoreRecorder>();
this.gameObject.AddComponent<DiskFactory>();
userGUI = this.gameObject.AddComponent<UserGUI>();
scoreRecorder = Singleton<ScoreRecorder>.Instance;
director.currentSceneController.LoadResources();
}

// Update is called once per frame
void Update () {//manage game state
/**
*游戏进入下一轮,增加轮数,同时重置飞碟数重置,分数不重置
*/
if(actionManager.numberOfDisks == 0 && gameState == 1) {
gameRound = (gameRound + 1) % (totalRound + 1);
throwPeriod = (totalRound - gameRound)/2;
//从游戏工厂中获得一定数目的飞碟放入飞碟队列
DiskFactory df = Singleton<DiskFactory>.Instance;
for (int i = 0; i < numberOfDisks; i++)
{
disks.Enqueue(df.getDisk(gameRound));
}
//将每个动作挂在到disks链表的每个元素上面
actionManager.startGame(disks);
actionManager.numberOfDisks = numberOfDisks;
}

if(gameRound == 0) {//初始化
gameState = 0;
actionManager.numberOfDisks = 0;
}

/**
*判断飞碟扔出的时间间隔
*/
if(gameState == 1) {
if(timeInterval > throwPeriod) {
randomThrow();
timeInterval = 0;
}
else {
timeInterval += Time.deltaTime;
}
}
}

/**
*随机扔出飞碟的位置
*/
//上文实现过,此处省略

//implementation of interfaces
//上文实现过,此处省略
}

这样打飞碟游戏的大体部分就完成,但还差一个记分员来记录游戏的得分:

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
public class ScoreRecorder : MonoBehaviour {
//计分
public int score;
//记录不同颜色和大小对应的分数
private Dictionary<Color, int> colorTable = new Dictionary<Color, int> ();
private Dictionary<int, int> sizeTable = new Dictionary<int, int> ();
//设置每种颜色和大小对应的分数
void Start () {
score = 0;
colorTable.Add(Color.yellow, 1);
colorTable.Add(Color.red, 2);
colorTable.Add(Color.blue, 3);
sizeTable.Add(1, 2);
sizeTable.Add(2, 4);
sizeTable.Add(3, 6);
}

public void Record (GameObject disk) {
score += colorTable[disk.GetComponent<DiskData>().color] + sizeTable[disk.GetComponent<DiskData>().size];
}
//十轮结束后分数重置
public void reset() {
score = 0;
}
}

上文中的FirstController其实已经是完整版了,所以只剩下用户面板的UserGUI了,这里的计分和显示关数依然用第一节课学的OnGUI()就可以实现了,并要用Upddate()调用之前定义的hit(因为打飞碟也是用户的动作呀~):

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
public class UserGUI : MonoBehaviour {
private IUserAction action;
bool isFirst = true;
private GameObject cam;
GUIStyle buttonStyle;
GUIStyle labelStyle;
// Use this for initialization
void Start () {
action = Director.getInstance().currentSceneController as IUserAction;
buttonStyle = new GUIStyle("button");
buttonStyle.fontSize = Screen.width/30;
buttonStyle.alignment = TextAnchor.MiddleCenter;
labelStyle = new GUIStyle("label");
labelStyle.alignment = TextAnchor.MiddleCenter;
labelStyle.fontSize = Screen.height/40;
labelStyle.normal.textColor = Color.white;
}

void OnGUI() {
if(action.getGameState() == 0 && GUI.Button(new Rect(Screen.width/2 - Screen.width/12, Screen.height/2 - Screen.height/16, Screen.width/6, Screen.height/8),"Restart", buttonStyle)) {
action.getGameStart();
}
else if(action.getGameState() == 0)
GUI.Label(new Rect(Screen.width/2 - Screen.width/12, Screen.height/5 + Screen.height/16, Screen.width/6, Screen.height/8), "Score: "+ action.getScore(), labelStyle);
else {
GUI.Label(new Rect(Screen.width/10, Screen.height/5, Screen.width/10, Screen.height/10), "Round: " + action.getRound(), labelStyle);
GUI.Label(new Rect(Screen.width/10, Screen.height/5 + 100, Screen.width/10, Screen.height/10), "Score: "+ action.getScore(), labelStyle);
}
}

void Update () {
if (Input.GetButtonDown ("Fire1")) {
Vector3 mp = Input.mousePosition;
action.hit(mp);
}
}
}