游戏对象工厂:打飞碟游戏 这篇博客会介绍一个打飞碟游戏,主要是引入一个游戏对象的创建与回收机制。
如果每次需要一个新的游戏对象的时候都重新创建它,用完了又马上销毁它,会造成资源的浪费以及游戏性能的下降。本次作业中的飞碟就是一个很好的例子,需要在游戏过程中源源不断地飞出,之后又被击落或者飞出屏幕。
因此,首先创建飞碟工厂的类:在游戏中,这个工厂需要根据用户(游戏整体)的需求借出飞碟,当用户不再使用的时候就由工厂将飞碟回收。如果用户需要的时候工厂也没有飞碟了,工厂就再去生产(加载新的预制)。
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 ) { 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>(); } 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 ) { DiskData temp = null ; foreach (DiskData i in used) { if (i.gameObject.GetInstanceID() == disk.GetInstanceID()) { temp = i; } } if (temp != null ) { 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; } 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); } } public void startGame (Queue <GameObject> diskQueue ) { Debug.Log("start game" ); foreach (GameObject temp in diskQueue) { RunAction(temp, GetSSAction(), this ); } } 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); 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 ; private UserGUI userGUI; 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(); } void Update ( ) { 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)); } 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; } } } }
这样打飞碟游戏的大体部分就完成,但还差一个记分员来记录游戏的得分:
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; 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); } } }