Unity3D 学习笔记 - Garen Pick the Balls 捡球小游戏设计
注:本游戏开发环境为Unity 3D v4.6
老师说这星期作业比较简单,所以我决定写得规整一些。
开发时间:8小时
游戏要求:
小游戏争分夺秒:随机位置生成七个球,控制主角在地图拾取七个球,十秒钟内必须完成,否则失败
具体要求:
1 随机位置在地图上生成七个球(球可以用系统自带的球体)
2 用键盘控制本课程中的角色移动,鼠标左键攻击到达打击帧时,拾取碰到的球。
3 通过Time类显示每次拾取球花费的时间
经过试验,十秒根本捡不完= =,15秒还可以。。。
首先来看看帅气万分的主角Garen哥
。。。
额,忘了穿衣服
将tex_Garen拖到transform_Garon的renderer上
。。。按90年代的标准还是挺帅的。。。
当然指的是公元3世纪90年代。。。
考虑到后面的编程中会使用到盒式碰撞器Box Collider来触发,因此在这里为Garen哥加上Box Collider和Rigidbody组件。
Box Collider的形状自行调整。
都加上去之后,新建预制GarenPrefab,保存为预制。
游戏类图设计:
1. 由于游戏模型简单,GUI就交给GameModel你了!
2. 裁判类Judge保持一个列表,列表中保存着此时Garen“身旁”的球对象
2.1 每个球自带触发器,当Garen的盒式碰撞器进入了球的触发范围的时候(OnTriggerEnter()),球触发器发出一个信息。裁判类Judge接收到这个信息,把这个球对象加入到自己的列表当中。
2.2 Garen离开(OnTriggerExit())时,球触发器发出信息。Judge将这个球从他的列表中移除。
2.3 Garen攻击时,发出一个信息。Judge收到这个信息,通知球工厂BallFactory将自己列表中所有的球回收。
2.4 GarenController, Ball, Judge之间的通信,采用观察者模式,C#中利用Delegate,event实现
3. 本设计采用Legacy动画,GarenController中同时实现输入采集和动画。不得不指出Legacy动画实现非常麻烦,要自己设计状态机。这里可能会有点小bug,不在此修复,下星期学了Mecanim之后用Mecanim实现。
4. 这次裁判类总算认真做裁判工作。Judge中保存了游戏中所有重要的数据。GameModel负责调用iJudgeInterface接口来控制游戏状态,计时等等。
BaseCode:
using UnityEngine; using System.Collections; using Com.GarenPickingBalls; public class BaseCode : MonoBehaviour { // Use this for initialization void Start () { SceneController sceneController = SceneController.GetInstance (); Judge judge = Judge.GetInstance (); UnityEngine.Camera.main.gameObject.AddComponent<GameModel> (); } // Update is called once per frame void Update () { } }
SceneController:
using UnityEngine; using System.Collections; namespace Com.GarenPickingBalls{ public class SceneController : System.Object { public Camera mainCam; public GameObject Garen; void initScene (){ GameObject floor = GameObject.CreatePrimitive (PrimitiveType.Plane); floor.name = "floor"; Light light = new GameObject ().AddComponent<Light> (); light.gameObject.name = "light"; light.type = LightType.Point; light.transform.position = new Vector3 (0, 2.4f, 0); mainCam = UnityEngine.Camera.main; mainCam.transform.position = new Vector3 (0.185f, 7.734f, 6.42f); mainCam.transform.eulerAngles = new Vector3 (54.115f, 183.6038f, 3.777f); GameObject GarenPrefab = Resources.LoadAssetAtPath ("Assets/Resources/Garen/GarenPrefab.prefab", typeof(GameObject)) as GameObject; Garen = GameObject.Instantiate (GarenPrefab, Vector3.zero, Quaternion.identity) as GameObject; Garen.name = "Garen"; Garen.AddComponent<GarenController>(); } //Singleton public static SceneController _instance; public static SceneController GetInstance() { if (_instance == null){ _instance = new SceneController(); } return _instance; } SceneController (){ initScene (); } } }
GameModel:
using UnityEngine; using System.Collections; using Com.GarenPickingBalls; public class GameModel : MonoBehaviour { BallFactory ballFactory = BallFactory.GetInstance(); iJudgeInterface iJudge = Judge.GetInstance () as iJudgeInterface; void OnGUI () { GUI.Label (new Rect(20f,20f,300f,20f), "Garon Picking Ball Game"); if (iJudge.getGameStatus () == 0) { GUI.Label (new Rect(20f, 40f, 300f, 20f), "Press Space Bar To Begin"); } if (iJudge.getGameStatus () == 1) { GUI.Label (new Rect(20f, 40f, 300f, 20f), iJudge.getRemainingTime().ToString("0") + " seconds left"); GUI.Label (new Rect(20f, 60f, 300f, 20f), "Garon Picked Up this ball at: " + iJudge.getLastGaronHit().ToString("0.0") + " seconds"); } if (iJudge.getGameStatus () == 2) { GUI.Label (new Rect(20f, 40f, 300f, 20f), "Lose, press spacebar to restart"); } if (iJudge.getGameStatus () == 3) { GUI.Label (new Rect(20f, 40f, 300f, 20f), "Win, press spacebar to restart"); } } // Update is called once per frame void Update () { if (iJudge.getGameStatus() == 0 || iJudge.getGameStatus() == 2 || iJudge.getGameStatus() == 3) { if (Input.GetKey(KeyCode.Space)){ for (int i = 0; i < 7; i++) { Vector3 pos = new Vector3(Random.Range(-5f,5f), 0, Random.Range(-5f,5f)); GameObject ball = ballFactory.getNewBall(pos); } iJudge.startGame(); } } else if (iJudge.getGameStatus() == 1) { iJudge.stepTime(); if (iJudge.getRemainingTime() < 0){ iJudge.loseGame(); } else if (iJudge.getBallPickedUp() == 7){ iJudge.winGame(); } } } }
Judge:
using UnityEngine; using System.Collections; using System.Collections.Generic; using Com.GarenPickingBalls; public delegate void onHitActionHandler (); public delegate void onGaronInContactWithBall (GameObject ball); public delegate void onGaronLeavingTheBall (GameObject ball); public interface iJudgeInterface { int getGameStatus(); void startGame(); void loseGame(); void winGame(); float getLastGaronHit(); float getRemainingTime (); void stepTime (); int getBallPickedUp (); } public class Judge : System.Object , iJudgeInterface { List <GameObject> ballInContact; BallFactory ballFactory; float TimeLimit = 15f; float t = 0f; float lastGaronHit = 0f; int ballPickedUp = 0; //game status: 0 for init, 1 for gaming, 2 for win, 3 for lose; int gameStatus = 0; //getting message from ball OnTriggerEnter() void GaronEnterContactWithBall (GameObject withBall){ ballInContact.Add (withBall); } //getting message from ball OnTriggerExit() void GaronExitContacWithBall (GameObject withBall) { ballInContact.Remove (withBall); } //getting message from Garon onHit() void GaronHit (){ if (ballInContact.Count != 0){ for (int i = 0; i < ballInContact.Count; i++) { ballFactory.releaseBall(ballInContact[i]); } ballPickedUp += ballInContact.Count; lastGaronHit = t; ballInContact.Clear (); } } public int getGameStatus (){ return gameStatus; } public void startGame(){ t = 0f; lastGaronHit = 0f; ballPickedUp = 0; gameStatus = 1; } public void winGame() { gameStatus = 3; } public void loseGame() { gameStatus = 2; ballFactory.recycleAllBalls (); } public float getLastGaronHit() { return lastGaronHit; } public float getRemainingTime (){ return TimeLimit - t; } public void stepTime () { t += Time.deltaTime; } public int getBallPickedUp () { return ballPickedUp; } //Singleton public static Judge _instance; public static Judge GetInstance() { if (_instance == null){ _instance = new Judge(); } return _instance; } Judge() { ballInContact = new List<GameObject> (); ballFactory = BallFactory.GetInstance (); GarenController.onHit += GaronHit; ballTrigger.enterContact += GaronEnterContactWithBall; ballTrigger.exitContact += GaronExitContacWithBall; } }
GarenController:
using UnityEngine; using System.Collections; using Com.GarenPickingBalls; public class GarenController : MonoBehaviour { private iJudgeInterface iJudge = Judge.GetInstance () as iJudgeInterface; private float speed = 3f; private float angularSpeed = 10f; private Animation animation; private AnimationState run; private AnimationState idle; private AnimationState attack; private bool attackingFlag = false; public static event onHitActionHandler onHit; // Use this for initialization void Start () { animation = GetComponent<Animation> (); run = animation["Run"]; idle = animation["Idle"]; attack = animation["Attack1"]; idle.wrapMode = WrapMode.Loop; AnimationEvent aev = new AnimationEvent (); aev.functionName = "attackOnHit"; aev.time = 0.3f; attack.clip.AddEvent (aev); AnimationEvent finishedAtt = new AnimationEvent (); finishedAtt.functionName = "finishedAttack"; finishedAtt.time = attack.clip.length / 2f; attack.clip.AddEvent (finishedAtt); Idle (); } // Update is called once per frame //Some bugs in here, gonna use new mode next time anyway... void Update () { if (iJudge.getGameStatus () == 1) { if (Input.GetKey (KeyCode.A)) { transform.position += Vector3.right * speed * Time.deltaTime; transform.rotation = Quaternion.LookRotation (Vector3.RotateTowards(transform.forward, Vector3.right, angularSpeed * Time.deltaTime, 0.0f)); Run (); } if (Input.GetKey (KeyCode.D)){ transform.position += Vector3.left * speed * Time.deltaTime; transform.rotation = Quaternion.LookRotation (Vector3.RotateTowards(transform.forward, Vector3.left, angularSpeed * Time.deltaTime, 0.0f)); Run (); } if (Input.GetKey (KeyCode.S)){ transform.position += Vector3.forward * speed * Time.deltaTime; transform.rotation = Quaternion.LookRotation (Vector3.RotateTowards(transform.forward, Vector3.forward, angularSpeed * Time.deltaTime, 0.0f)); Run (); } if (Input.GetKey (KeyCode.W)){ transform.position += Vector3.back * speed * Time.deltaTime; transform.rotation = Quaternion.LookRotation (Vector3.RotateTowards(transform.forward, Vector3.back, angularSpeed * Time.deltaTime, 0.0f)); Run (); } if (!Input.anyKey && !attackingFlag) { Idle(); } if (Input.GetMouseButtonDown(0)) { attackingFlag = true; Attack(); } } else{ Idle (); } } public void Idle (float speed = 1f) { idle.speed = speed; idle.wrapMode = WrapMode.Loop; animation.CrossFade (idle.clip.name); } public void Run (float speed = 2f) { run.speed = speed; // run.wrapMode = WrapMode.Loop; animation.CrossFade (run.clip.name); } public void Attack (float speed = 1f) { attack.speed = speed; animation.CrossFade (attack.clip.name); } public void finishedAttack (){ attackingFlag = false; } //Added to Animation Events, sending message out to delegate notifying Garon has hit public void attackOnHit (){ onHit (); } }
BallFactory & ballTrigger
using UnityEngine; using System.Collections; using System.Collections.Generic; using Com.GarenPickingBalls; public class BallFactory : System.Object { List<GameObject> usedBalls; List<GameObject> usingBalls; int ballCount = 0; public GameObject getNewBall(Vector3 position){ GameObject aBall = null; if (usedBalls.Count == 0){ aBall = GameObject.CreatePrimitive(PrimitiveType.Sphere); aBall.transform.localScale = new Vector3 (0.5f, 0.5f, 0.5f); aBall.transform.position = position; aBall.name = "ball" + ballCount.ToString(); ballCount++; Collider bc = aBall.GetComponent<Collider>(); bc.isTrigger = true; aBall.AddComponent<ballTrigger>(); usingBalls.Add(aBall); } else { aBall = usedBalls[0]; usedBalls.Remove(aBall); usingBalls.Add (aBall); aBall.SetActive (true); aBall.transform.position = position; } return aBall; } public void releaseBall (GameObject ballToRelease){ ballToRelease.SetActive (false); usingBalls.Remove (ballToRelease); usedBalls.Add (ballToRelease); } public void recycleAllBalls (){ while (usingBalls.Count != 0) { releaseBall(usingBalls[0]); } } //Singleton public static BallFactory _instance; public static BallFactory GetInstance() { if (_instance == null){ _instance = new BallFactory(); } return _instance; } BallFactory (){ usedBalls = new List<GameObject> (); usingBalls = new List<GameObject> (); } } public class ballTrigger : MonoBehaviour{ public static event onGaronInContactWithBall enterContact; public static event onGaronLeavingTheBall exitContact; //notifying that Garon has entered the trigger zone void OnTriggerEnter (Collider other){ //delegate enterContact (this.gameObject); } //notifying that Garon has exited the trigger zone void OnTriggerExit (Collider other) { //delegate exitContact (this.gameObject); } }
。。。
XDDDD