Unity3D 学习笔记 - ShootThatUFO! 打飞碟小游戏设计
注:本游戏设计环境为Unity 3D 4.6
游戏要求:
- 假设有一支枪在摄像机位置,在放置三个小球作为距离标记,调整视角直到小球在下中部
- 将鼠标所在平面坐标,转换为子弹(球体)射出的角度方向。子弹使用物理引擎,初速度恒定。
- 游戏要分多个 round , 飞碟数量每个 round 都是 n 个,但色彩,大小;发射位置,速度,角度,每次发射数量按预定规则变化。
- 用户按空格后,321倒数3秒,飞碟飞出(物理引擎控制),点击鼠标,子弹飞出。飞碟落地,或被击中,则准备下一次射击。
-
以下是一些技术要求:
- 飞碟用一个带缓存的工厂生产,template 中放置预制的飞碟对象
一、游戏UML类图设计
游戏最核心的几个部分是:
SceneController
GameModel
ActionManager
UFOFactory
BulletFactory
Judge
UserInterface
没有在UML类图中出现的两部分:
SelfDestruct,挂载在Bullet Prefab上作为飞碟预设
public class SelfDestruct : MonoBehaviour { public float t = 0; // Update is called once per frame void Update () { t += Time.deltaTime; if (t > 1.5f) { this.gameObject.GetComponent<shootBullet> ().Free(); BulletFactory.getInstance().releaseBullet(this.gameObject); SceneController.GetInstance().trials++; SceneController.GetInstance().okToShoot = true; } } }
Detect Collision,挂载在UFO Prefab上作为子弹预设
public class DetectCollision : MonoBehaviour { UFOFactory ufoFactory = UFOFactory.getInstance(); BulletFactory bulletFactory = BulletFactory.getInstance(); void OnTriggerEnter (Collider other){ SceneController.GetInstance ().onHittingUFO (); ufoFactory.releaseUFO (this.gameObject); other.gameObject.GetComponent<SelfDestruct> ().t = 0f; bulletFactory.releaseBullet (other.gameObject); SceneController.GetInstance ().okToShoot = true; } }
二、核心类分析
SceneController 场景控制类
在这个类存放了许多相关的游戏数据:游戏状态,关数,分数,最高得分,与关数相关的场景数据;同时也存放了主摄像机的实例
这个类扩展了两个接口:IJudgeEvents, IUserAction,分别由Judge,UserInterface使用
namespace Com.ShootThatUFO { public interface IJudgeEvents { void onHittingUFO (); void onNotHittingUFO (); int getTrial(); void gameFailed(); void updateHighscore(); void setGameStatus (int gameStatus); int getPoint (); int getHighscore(); int getRound (); } public interface IUserActions { //Game Status, 0 for init, 1 for in game, 2 for mid round, 3 for failed, 4 for game cleared int getGameStatus (); void gameStart(); int getRound (); int getUFONum (); int getPoint (); int getTrial (); int getHighscore (); void resetGame (); } public class SceneController : System.Object, IJudgeEvents, IUserActions { //Global Variables public static float GROUND = 0f; //Init public Camera mainCam; SceneController () { mainCam = UnityEngine.Camera.main; mainCam.transform.position = new Vector3 (0f, 2.56f, -10f); mainCam.transform.eulerAngles = new Vector3 (6.475f, 0f, 0f); mainCam.fieldOfView = 76f; mainCam.backgroundColor = Color.black; Physics.gravity = new Vector3 (0, -9.8f, 0); GameObject lightObj = new GameObject (); lightObj.name = "light"; Light light = lightObj.AddComponent<Light> (); light.type = LightType.Directional; } //Game Data public int gameStatus = 0; public float gameStartCountDown = 3f; public bool okToShoot = true; public int round = 0; public int point = 0; public int trials = 0; public int ufoToToss = 20; public int highscore = 0; public float[] intervalPerRound = {2f, 2f, 1f, 1f, 0.5f}; public float[] tossRange = {10f, 10f, 15f, 20f, 20f}; public float[] tossSpeed = {20f, 20f, 25f, 25f, 30f}; public float[] ufoScale = {0.5f, 0.8f, 1f}; public Color[] ufoColor = {Color.green, Color.yellow, Color.red}; public float[] tossMaxAngle = {0.6f,0.6f,0.6f,0.6f,0.7f}; public void updateHighscore (){ highscore = point; } //UI Interface public int getGameStatus (){ return gameStatus; } public void gameStart (){ gameStatus = 1; } public int getRound (){ return round; } public int getUFONum (){ return ufoToToss; } public int getPoint (){ return point; } public int getTrial (){ return trials; } public void resetGame (){ round = 0; point = 0; trials = 0; ufoToToss = 20; _base_code.gameModel.t = 0f; } public int getHighscore (){ return highscore; } //Judge Interface public void onHittingUFO (){ point += 10; } public void onNotHittingUFO (){ trials++; } public void gameFailed (){ gameStatus = 3; } public void setGameStatus (int _gameStatus){ this.gameStatus = _gameStatus; } //Singleton private static SceneController _instance; private BaseCode _base_code; public static SceneController GetInstance() { if (_instance == null) { _instance = new SceneController (); } return _instance; } public BaseCode getBaseCode(){ return _base_code; } internal void setBaseCode(BaseCode bc){ if (_base_code == null) { _base_code = bc; } } } //End of Namespace }
GameModel 游戏模型
这个类是游戏中最核心的部分,和SceneController, ActionManager, UFOFactory, BulletFactory聚合,负责投出飞碟,获取用户鼠标输入,发射子弹
飞碟投出颜色,大小,射出角度,速度,方向,均有此类随机产生,随机范围由SceneController中静态决定
public class GameModel : MonoBehaviour, IU3dActionCompleted { SceneController sceneController; public ActionManager actionManager; public UFOFactory ufoFactory; public BulletFactory bulletFactory; public float t = 0f; // Use this for initialization void Start () { sceneController = SceneController.GetInstance (); actionManager = ActionManager.GetInstance (); ufoFactory = UFOFactory.getInstance (); actionManager = ActionManager.GetInstance (); ufoFactory = UFOFactory.getInstance (); bulletFactory = BulletFactory.getInstance (); } public void OnActionCompleted (U3dAction action){ sceneController.okToShoot = true; ufoFactory.releaseUFO (action.gameObject); } // Update is called once per frame void Update () { if (Input.GetMouseButtonDown (0) && sceneController.getGameStatus() == 1 && sceneController.okToShoot) { Ray ray = sceneController.mainCam.ScreenPointToRay (Input.mousePosition); GameObject bullet = bulletFactory.getNewBullet(); actionManager.applyShootBullet(bullet, ray.direction); sceneController.okToShoot = false; } if (sceneController.getGameStatus() == 1){ t += Time.deltaTime; if (t > sceneController.intervalPerRound[sceneController.round]) { t = 0; if (sceneController.ufoToToss != 0){ Vector3 tossTo = new Vector3 (Random.Range(-sceneController.tossRange[sceneController.round], sceneController.tossRange[sceneController.round]), SceneController.GROUND, 10f); float tossAngle = Random.Range(0.56f, sceneController.tossMaxAngle[sceneController.round]); float tossSpeed = Random.Range(15f, sceneController.tossSpeed[sceneController.round]); // Debug.Log ("Tossing! " + tossTo.x + " " + tossAngle + " " + tossSpeed); GameObject aUFO = ufoFactory.getNewUFO (sceneController.ufoScale[Random.Range(0, 3)], sceneController.ufoColor[Random.Range(0, 3)]); actionManager.applyU3dTossUFO (aUFO, aUFO.transform.position, tossTo, tossAngle, tossSpeed, this); sceneController.ufoToToss--; } else { sceneController.round++; sceneController.ufoToToss = 20; sceneController.gameStatus = 2; } } } } }
ActionManager 动作控制类
控制动作的发生,包括UFO的投出,子弹的发射。并未通过复杂数学来实现,而是在void setting()中为rigidbody添加Impulse类型力。
要注意的是,在回收动作时,要为rigidbody的velocity, angularVelocity设Vector3.zero,否则下次再添加Impulse时会出问题。
public class ActionManager :System.Object { private static ActionManager _instance; public static ActionManager GetInstance(){ if (_instance == null) { _instance = new ActionManager(); } return _instance; } public U3dAction applyU3dTossUFO (GameObject obj, Vector3 _startPoint, Vector3 _finishPoint, float _initialAngle, float _speed, IU3dActionCompleted _completed){ U3dTossUFO UTU = obj.AddComponent<U3dTossUFO> (); UTU.setting (_startPoint, _finishPoint, _initialAngle, _speed, _completed); return UTU; } public U3dAction applyU3dTossUFO (GameObject obj, Vector3 _startPoint, Vector3 _finishPoint, float _initialAngle, float _speed){ return applyU3dTossUFO (obj, _startPoint, _finishPoint, _initialAngle, _speed, null); } public U3dAction applyShootBullet (GameObject obj, Vector3 direction){ shootBullet sb = obj.AddComponent<shootBullet> (); sb.setting (direction); return sb; } } public class U3dActionException : System.Exception {} public interface IU3dActionCompleted { void OnActionCompleted (U3dAction action); } public class U3dAction : MonoBehaviour { public void Free(){} } public class U3dActionAuto : U3dAction {} public class U3dActionMan : U3dAction {} public class U3dTossUFO : U3dActionAuto { public Vector3 startPoint; public Vector3 finishPoint; public float forceStrength; public float initialAngle; private IU3dActionCompleted monitor = null; // initialAngle in Radial public void setting (Vector3 _startPoint, Vector3 _finishPoint, float _initialAngle, float _speed, IU3dActionCompleted _monitor){ this.startPoint = _startPoint; this.finishPoint = _finishPoint; this.initialAngle = _initialAngle; this.forceStrength = _speed; this.monitor = _monitor; Vector3 force = Vector3.Slerp(Vector3.Normalize(finishPoint - startPoint), Vector3.up, initialAngle); Debug.Log (force + " " + forceStrength); this.gameObject.rigidbody.AddForce (force * forceStrength, ForceMode.Impulse); } void FixedUpdate() { if (transform.position.y < SceneController.GROUND + 0.1f) { if (monitor != null){ monitor.OnActionCompleted (this); } this.Free(); } } public void Free(){ this.gameObject.rigidbody.velocity = Vector3.zero; this.gameObject.rigidbody.angularVelocity = Vector3.zero; Destroy (this); } } public class shootBullet : U3dActionAuto { Vector3 direction; float t = 0; public void setting (Vector3 direction){ this.gameObject.rigidbody.AddForce (direction * 50f, ForceMode.Impulse); } void FixedUpdate() { } void OnTriggerEnter() { this.Free (); } public void Free(){ this.gameObject.rigidbody.velocity = Vector3.zero; this.gameObject.rigidbody.angularVelocity = Vector3.zero; Destroy (this); } }
UFOFactory 飞碟工厂
维持两个List<GameObject>分别保存用过的飞碟,正在使用的飞碟,仅在没有用过的飞碟来重用的时候才Instantiate新的飞碟
外部通过get, release两个方法来获取,消除飞碟
public class UFOFactory : System.Object { //Singleton Implementation private static UFOFactory _instance = null; public static UFOFactory getInstance() { if (_instance == null) { _instance = new UFOFactory(); } return _instance; } //Initalizing private GameObject _UFOPrefab; private GameObject _ExplosionPrefab; UFOFactory (){ // GameObject UFOPrimitive = GameObject.CreatePrimitive (PrimitiveType.Cylinder); // UFOPrimitive.transform.localScale = new Vector3 (1f, 0.1f, 1f); // UFOPrimitive.AddComponent<DetectCollision> (); // Rigidbody uRB = UFOPrimitive.AddComponent<Rigidbody> (); // uRB.drag = 1f; // UFOPrimitive.collider.isTrigger = true; // UnityEditor.AssetDatabase.CreateFolder ("Assets/Resources/Prefab"); // _UFOPrefab = UnityEditor.PrefabUtility.CreatePrefab ("Assets/Resources/Prefab/UFOPrefab.prefab", UFOPrimitive); _UFOPrefab = Resources.Load ("Prefab/UFOPrefab") as GameObject; // UnityEngine.GameObject.Destroy (UFOPrimitive); _ExplosionPrefab = Resources.Load ("Exploson7", typeof (GameObject)) as GameObject; usedUFO = new List<GameObject> (); usingUFO = new List<GameObject> (); } //List of Gameobjects private List<GameObject> usedUFO; private List<GameObject> usingUFO; public GameObject getNewUFO (float scale, Color color) { GameObject aUFO; if (usedUFO.Count == 0) { aUFO = UnityEngine.GameObject.Instantiate(_UFOPrefab, new Vector3 (0f, SceneController.GROUND + 0.11f, -10f), Quaternion.identity) as GameObject; usingUFO.Add (aUFO); aUFO.tag = "Finish"; } else { aUFO = usedUFO[0]; usedUFO.Remove(aUFO); usingUFO.Add(aUFO); aUFO.SetActive (true); } aUFO.transform.position = new Vector3 (0f, SceneController.GROUND + 0.11f, -10f); aUFO.transform.localScale = new Vector3 (1f, 0.07f, 1f); aUFO.transform.localScale = aUFO.transform.localScale * scale; aUFO.renderer.material.color = color; return aUFO; } public void releaseUFO (GameObject UFOtoRelease){ usingUFO.Remove (UFOtoRelease); usedUFO.Add (UFOtoRelease); UFOtoRelease.SetActive (false); UnityEngine.Object.Instantiate (_ExplosionPrefab, UFOtoRelease.transform.position, Quaternion.identity); } }
BulletFactory 子弹工厂
与UFO工厂类似
public class BulletFactory : System.Object { //Singleton Implementation private static BulletFactory _instance = null; public static BulletFactory getInstance() { if (_instance == null) { _instance = new BulletFactory(); } return _instance; } //Initalizing private GameObject _BulletPrefab; BulletFactory (){ // GameObject BulletPrimitive = GameObject.CreatePrimitive (PrimitiveType.Sphere); // UFOPrimitive.transform.localScale = new Vector3 (0.3f, 0.3f, 0.3f); // Rigidbody bRB = BulletPrimitive.AddComponent<Rigidbody> (); // BulletPrimitive.AddComponent<SelfDestruct> (); // _BulletPrefab = UnityEditor.PrefabUtility.CreatePrefab ("Assets/Resources/Prefab/bulletPrefab.prefab", BulletPrimitive); usedBullet = new List<GameObject> (); usingBullet = new List<GameObject> (); } //List of Gameobjects private List<GameObject> usedBullet; private List<GameObject> usingBullet; public GameObject getNewBullet () { GameObject bullet; if (usedBullet.Count == 0) { bullet = UnityEngine.Object.Instantiate (Resources.Load ("Prefab/Bullet", typeof (GameObject))) as GameObject; usingBullet.Add (bullet); } else { Debug.Log ("Here"); bullet = usedBullet[0]; bullet.transform.position = UnityEngine.Camera.main.transform.position; bullet.SetActive(true); usedBullet.Remove(bullet); usingBullet.Add (bullet); } bullet.GetComponent<SelfDestruct> ().t = 0f; return bullet; } public void releaseBullet (GameObject bullettoRelease){ bullettoRelease.SetActive (false); usingBullet.Remove (bullettoRelease); usedBullet.Add (bullettoRelease); } }
Judge 裁判类
通过检测SceneController中的关键游戏数据,来判断游戏状态
//这些关键数据是否存放在裁判类中会更好?
public class Judge : MonoBehaviour{ IJudgeEvents iJudege; void Start (){ iJudege = SceneController.GetInstance() as IJudgeEvents; } void Update (){ if (iJudege.getPoint() > iJudege.getHighscore()) { iJudege.updateHighscore(); } if (iJudege.getRound() == 5) { iJudege.setGameStatus(4); } if (iJudege.getTrial() > 9) { iJudege.gameFailed(); } } }
UserInterface 界面类
控制游戏界面,同时接受用户启动游戏的命令(空格)
public class UserInterface : MonoBehaviour { IUserActions userAction; public bool spaceHitted = false; public float gameStartTimer = 3f; // Use this for initialization void Start () { userAction = SceneController.GetInstance () as IUserActions; } void OnGUI () { // Make a background box if (userAction.getGameStatus() == 1) { GUI.Label (new Rect(20,20,80,20), "Round " + (userAction.getRound() + 1)); GUI.Label (new Rect (20, 40, 80, 20), userAction.getUFONum () + " to Go"); } if (userAction.getGameStatus() == 0 || userAction.getGameStatus() == 2) { GUI.Label (new Rect(20,20,300,20), "Shoot That UFO! Hit Space Bar to Begin!"); GUI.Label (new Rect (20, 40, 100, 20), "Time to start: " + gameStartTimer.ToString("0.0")); GUI.Label (new Rect (20, 100, 100, 20), "Next Round: " + (userAction.getRound() + 1)); } if (userAction.getGameStatus () == 3) { GUI.Label (new Rect (20, 20, 200, 20), "Failed! Hit Spacebar to Restart."); GUI.Label (new Rect (20, 40, 100, 20), "Time to start: " + gameStartTimer.ToString("0.0")); } if (userAction.getGameStatus () != 4) { GUI.Label (new Rect (20, 80, 150, 20), "You have " + (10f - userAction.getTrial()) + " bullets left."); GUI.Label (new Rect (700, 20, 100, 20), "Highscore: " + userAction.getHighscore()); } if (userAction.getGameStatus () == 4) { GUI.Label (new Rect(20,20,150,20), "Game Cleared!"); } GUI.Label (new Rect (20, 60, 150, 20), "Your score is " + userAction.getPoint()); } // Update is called once per frame void FixedUpdate () { if (userAction.getGameStatus () != 1) { if (Input.GetKey (KeyCode.Space)) { spaceHitted = true; if (userAction.getGameStatus () == 3){ userAction.resetGame(); } } if (spaceHitted) { gameStartTimer -= Time.deltaTime; } if (gameStartTimer < 0f) { userAction.gameStart(); gameStartTimer = 3f; spaceHitted = false; } } } }
BaseCode 基础代码
将此代码挂载到游戏中,此代码将挂载Judge, GameModel, UserInterface到主摄像机中
public class BaseCode : MonoBehaviour { SceneController sceneController; public Judge judge; public GameModel gameModel; public UserInterface ui; // Use this for initialization void Start () { sceneController = SceneController.GetInstance (); sceneController.setBaseCode (this); gameModel = UnityEngine.Camera.main.gameObject.AddComponent <GameModel> (); judge = UnityEngine.Camera.main.gameObject.AddComponent <Judge> (); ui = UnityEngine.Camera.main.gameObject.AddComponent <UserInterface> (); } }