编程实践——编写一个简单的鼠标打飞碟(Hit UFO)游戏
游戏内容要求:
游戏有 n 个 round,每个 round 都包括10 次 trial;
每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
每个 trial 的飞碟有随机性,总体难度随 round 上升;
鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
游戏的要求:
使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
尽可能使用前面 MVC 结构实现人机交互与游戏模型分离
UserGUI.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour
{
private IUserAction action;
readonly float roundTime = 40f;
GUIStyle style;
GUIStyle buttonStyle;
GUIStyle pauseStyle;
GUIStyle resultStyle;
void Start()
{
action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
style = new GUIStyle();
style.fontSize = 20;
buttonStyle = new GUIStyle("button");
buttonStyle.fontSize = 30;
pauseStyle = new GUIStyle("button");
pauseStyle.alignment = TextAnchor.MiddleCenter;
pauseStyle.fontSize = 15;
resultStyle = new GUIStyle();
resultStyle.fontSize = 50;
resultStyle.alignment = TextAnchor.MiddleCenter;
resultStyle.normal.textColor = Color.white;
}
void OnGUI()
{
GUI.Box(new Rect(15, 15, 120, 50), "");
if (Input.GetButtonDown("Fire1") && action.GetGameState() == GameState.RUNNING)
{
Vector3 pos = Input.mousePosition;
action.Hit(pos);
}
if (action.GetGameState() != GameState.START)
{
GUI.Label(new Rect(0, 0, 80, 20), "Round " + (action.GetRound() + 1).ToString(), style);
GUI.Label(new Rect(0, 20, 80, 20), "Time: " + (roundTime - (int)action.GetTime()).ToString(), style);
GUI.Label(new Rect(0, 40, 80, 20), "Score: " + action.GetScore().ToString(), style);
}
if (action.GetGameState() == GameState.START && GUI.Button(new Rect(320, 280, 130, 55), "Start", buttonStyle))
{
action.SetGameState(GameState.RUNNING);
}
else if (action.GetGameState() == GameState.RUNNING && GUI.Button(new Rect(695, 5, 55, 30), "pause", pauseStyle))
{
action.SetGameState(GameState.PAUSE);
action.Pause();
}
else if (action.GetGameState() == GameState.PAUSE && GUI.Button(new Rect(695, 5, 55, 30), "run", pauseStyle))
{
action.SetGameState(GameState.RUNNING);
action.Run();
}
else if (action.GetGameState() == GameState.OVER)
{
if (GUI.Button(new Rect(320, 280, 130, 55), "Restart", buttonStyle))
action.Restart();
GUI.Label(new Rect(285, 130, 200, 50), "Your score is " + action.GetScore().ToString() + "!", resultStyle);
}
}
}
IUserAction.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum GameState { START, RUNNING, OVER, PAUSE }
public interface IUserAction
{
void Restart();
void Pause();
void Run();
void Hit(Vector3 pos);
int GetScore();
float GetTime();
int GetRound();
void SetGameState(GameState state);
GameState GetGameState();
}
Singleton.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
protected static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = (T)FindObjectOfType(typeof(T));
if (instance == null)
{
Debug.LogError("An instance of " + typeof(T)
+ " is needed in the scene, but there is none.");
}
}
return instance;
}
}
}
DiskData.cs
public class DiskData : MonoBehaviour
{
public Vector3 size;
public Color color;
public float speed;
public Vector3 direction;
public int score;
}
DiskFactory.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskFactory : MonoBehaviour
{
public GameObject disk; //飞碟预制体
private List<DiskData> used = new List<DiskData>(); //正在被使用的飞碟列表
private List<DiskData> free = new List<DiskData>(); //空闲的飞碟列表
public GameObject GetDisk(int round)
{
disk = null;
if (free.Count > 0)
{
disk = free[0].gameObject;
free.Remove(free[0]);
}
else
{
disk = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/disk"), Vector3.zero, Quaternion.identity);
disk.AddComponent<DiskData>();
}
// round 1: 全是黄色disk,最慢
// round 2: 40%可能出现黄色,60%可能出现红色(更快)
// round 3: 20%可能出现黄色,30%可能出现红色,50%可能出现黑色(更快 or 同时出现?)
int start = 0;
int selectedColor = Random.Range(start, round * 500);
if (selectedColor >= 500)
{
round = 2;
}
else if (selectedColor >= 200)
{
round = 1;
}
else
{
round = 0;
}
switch (round)
{
case 0:
{
disk.GetComponent<DiskData>().color = Color.yellow;
disk.GetComponent<DiskData>().speed = Random.Range(10f, 12f);
float startX = UnityEngine.Random.Range(-1f, 1f) < 0 ? -1 : 1;
disk.GetComponent<DiskData>().direction = new Vector3(startX, 1, 0);
disk.GetComponent<DiskData>().score = 1;
disk.GetComponent<Renderer>().material.color = Color.yellow;
break;
}
case 1:
{
disk.GetComponent<DiskData>().color = Color.red;
disk.GetComponent<DiskData>().speed = Random.Range(15f, 18f);
float startX = UnityEngine.Random.Range(-1f, 1f) < 0 ? -1 : 1;
disk.GetComponent<DiskData>().direction = new Vector3(startX, 1, 0);
disk.GetComponent<DiskData>().score = 2;
disk.GetComponent<Renderer>().material.color = Color.red;
break;
}
case 2:
{
disk.GetComponent<DiskData>().color = Color.blue;
disk.GetComponent<DiskData>().speed = Random.Range(10f, 15f);
float startX = UnityEngine.Random.Range(-1f, 1f) < 0 ? -1 : 1;
disk.GetComponent<DiskData>().direction = new Vector3(startX, 1, 0);
disk.GetComponent<DiskData>().score = 3;
disk.GetComponent<Renderer>().material.color = Color.blue;
break;
}
}
used.Add(disk.GetComponent<DiskData>());
return disk;
}
//回收飞碟
public void FreeDisk(GameObject disk)
{
for (int i = 0; i < used.Count; i++)
{
if (disk.GetInstanceID() == used[i].gameObject.GetInstanceID())
{
used[i].gameObject.SetActive(false);
free.Add(used[i]);
used.Remove(used[i]);
break;
}
}
}
}
FirstController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
readonly float roundTime = 40f;
private float speed = 1.5f; //发射一个飞碟的时间间隔
readonly int[] passScore = { 20, 50 };
private List<GameObject> disks = new List<GameObject>(); //飞碟队列
private int currentRound = 0; //回合
private float time = 0f; //记录时间间隔
private float currrentTime = 0f;
private GameState gameState = GameState.START;
public UserGUI userGUI;
public ScoreRecorder scoreRecorder; //计分
public DiskFactory diskFactory; //生成和回收飞碟
public FlyActionManager actionManager; //动作管理
// Start is called before the first frame update
void Start()
{
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
diskFactory = Singleton<DiskFactory>.Instance;
scoreRecorder = Singleton<ScoreRecorder>.Instance;
actionManager = gameObject.AddComponent<FlyActionManager>() as FlyActionManager;
userGUI = gameObject.AddComponent<UserGUI>() as UserGUI;
gameState = GameState.START;
time = 0f;
currentRound = 0;
currrentTime = 0;
LoadResources();
}
void Update()
{
if (gameState == GameState.RUNNING )
{
for (int i = 0; i < disks.Count; i++)
{
//飞碟飞出摄像机视野也没被打中
if ((disks[i].transform.position.y <= -4.5) && disks[i].gameObject.activeSelf == true)
{
diskFactory.FreeDisk(disks[i]);
disks.Remove(disks[i]);
scoreRecorder.Miss();
}
}
if (time > speed)
{
time = 0;
SendDisk();
}
else
{
time += Time.deltaTime;
}
if (currrentTime > roundTime)
{
currrentTime = 0;
if (currentRound < 2 && GetScore() >= passScore[currentRound])
{
currentRound++;
time = 0f;
}
else
{
GameOver();
}
}
else
{
currrentTime += Time.deltaTime;
}
}
}
private void GameOver()
{
gameState = GameState.OVER;
currrentTime = 40;
}
public void LoadResources()
{
//不需要加载,飞碟由diskFactory生产了
}
public void Hit(Vector3 pos)
{
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
bool isHit = false;
for (int i = 0; i < hits.Length; i++)
{
RaycastHit hit = hits[i];
if (hit.collider.gameObject.GetComponent<DiskData>() != null) //射线打中某物体
{
for (int j = 0; j < disks.Count; j++) //射中的物体要在飞碟列表中
{
if (hit.collider.gameObject.GetInstanceID() == disks[j].gameObject.GetInstanceID())
{
isHit = true;
}
}
if (!isHit) //如果没有打中,返回
{
return;
}
disks.Remove(hit.collider.gameObject); //从队列中移除
scoreRecorder.Record(hit.collider.gameObject); //记分员记录分数
StartCoroutine(WaitingParticle(0.08f, hit, diskFactory, hit.collider.gameObject)); //等0.08秒后执行回收飞碟,这一步很关键
break;
}
}
}
//暂停几秒后回收飞碟
IEnumerator WaitingParticle(float waitTime, RaycastHit hit, DiskFactory diskFactory, GameObject obj)
{
yield return new WaitForSeconds(waitTime);
//等待之后执行的动作
hit.collider.gameObject.transform.position = new Vector3(0, -9, 0);
diskFactory.FreeDisk(obj);
}
//发送飞碟
private void SendDisk()
{
GameObject disk = diskFactory.GetDisk(currentRound);
disks.Add(disk);
disk.SetActive(true);
//设置被隐藏了或是新建的飞碟的位置
float positionX = 16;
float ranY = Random.Range(1f, 4f);
float ranX = Random.Range(-1f, 1f) < 0 ? -1 : 1;
disk.GetComponent<DiskData>().direction = new Vector3(ranX, ranY, 0);
Vector3 position = new Vector3(-disk.GetComponent<DiskData>().direction.x * positionX, ranY, 0);
disk.transform.position = position;
//设置飞碟初始角度
float angle = Random.Range(15f, 20f);
actionManager.UFOFly(disk, angle);
if (disk.GetComponent<DiskData>().color == Color.blue)
{
GameObject disk1 = Instantiate(disk);
GameObject disk2 = Instantiate(disk);
disks.Add(disk1);
disk1.SetActive(true);
disk1.GetComponent<DiskData>().direction = new Vector3(ranX, ranY, 0);
disk1.transform.position = position;
actionManager.UFOFly(disk1, Random.Range(15f, 28f));
disks.Add(disk2);
disk2.SetActive(true);
disk2.GetComponent<DiskData>().direction = new Vector3(ranX, ranY, 0);
disk2.transform.position = position;
actionManager.UFOFly(disk2, Random.Range(15f, 28f));
}
}
public void Restart()
{
time = 0f;
currentRound = 0;
currrentTime = 0;
scoreRecorder.Reset();
gameState = GameState.RUNNING;
}
public int GetScore()
{
return scoreRecorder.score;
}
public void SetGameState(GameState state)
{
gameState = state;
}
public GameState GetGameState()
{
return gameState;
}
public float GetTime()
{
return currrentTime;
}
public int GetRound()
{
return currentRound;
}
public void Pause()
{
actionManager.Pause();
}
public void Run()
{
actionManager.Run();
}
}
Action.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSAction : ScriptableObject //动作
{
public bool enable = true; //是否正在进行此动作
public bool destroy = false; //是否需要被销毁
public GameObject gameobject; //动作对象
public Transform transform; //动作对象的transform
public ISSActionCallback callback; //回调函数
protected SSAction() { } //保证SSAction不会被new
public virtual void Start() //子类可以使用这两个函数
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
public class UFOFlyAction : SSAction
{
public float gravity = -5; //向下的加速度
private Vector3 startVector; //初速度向量
private Vector3 gravityVector = Vector3.zero; //加速度的向量,初始时为0
private float time; //已经过去的时间
private Vector3 currentAngle = Vector3.zero; //当前时间的欧拉角
public bool run = true;
private UFOFlyAction() { }
public static UFOFlyAction GetSSAction(Vector3 direction, float angle, float power)
{
//初始化物体将要运动的初速度向量
UFOFlyAction action = CreateInstance<UFOFlyAction>();
if (direction.x == -1)
{
action.startVector = Quaternion.Euler(new Vector3(0, 0, -angle)) * Vector3.left * power;
}
else
{
action.startVector = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * power;
}
return action;
}
public override void Update()
{
if (run)
{
//斜抛运动
//计算物体的向下的速度,v=at
time += Time.fixedDeltaTime;
gravityVector.y = gravity * time;
//位移模拟
transform.position += (startVector + gravityVector) * Time.fixedDeltaTime;
currentAngle.z = Mathf.Atan((startVector.y + gravityVector.y) / startVector.x) * Mathf.Rad2Deg;
transform.eulerAngles = currentAngle;
//如果物体y坐标小于-10,动作就做完了
if (this.transform.position.y < -10)
{
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
}
public override void Start() { }
}
public class SequenceAction : SSAction, ISSActionCallback
{
public List<SSAction> sequence; //动作的列表
public int repeat = -1; //-1就是无限循环做组合中的动作
public int start = 0; //当前做的动作的索引
public static SequenceAction GetSSAcition(int repeat, int start, List<SSAction> sequence)
{
SequenceAction action = ScriptableObject.CreateInstance<SequenceAction>();//让unity自己创建一个SequenceAction实例
action.repeat = repeat;
action.sequence = sequence;
action.start = start;
return action;
}
public override void Update()
{
if (sequence.Count == 0) return;
if (start < sequence.Count)
{
sequence[start].Update(); //一个组合中的一个动作执行完后会调用接口,所以这里看似没有start++实则是在回调接口函数中实现
}
}
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null)
{
source.destroy = false; //先保留这个动作,如果是无限循环动作组合之后还需要使用
this.start++;
if (this.start >= sequence.Count)
{
this.start = 0;
if (repeat > 0) repeat--;
if (repeat == 0)
{
this.destroy = true; //整个组合动作就删除
this.callback.SSActionEvent(this); //告诉组合动作的管理对象组合做完了
}
}
}
public override void Start()
{
foreach (SSAction action in sequence)
{
action.gameobject = this.gameobject;
action.transform = this.transform;
action.callback = this; //组合动作的每个小的动作的回调是这个组合动作
action.Start();
}
}
void OnDestroy()
{
//如果组合动作做完第一个动作突然不要它继续做了,那么后面的具体的动作需要被释放
}
}
public enum SSActionEventType : int { Started, Competeted }
public interface ISSActionCallback
{
void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null);
}
public class SSActionManager : MonoBehaviour, ISSActionCallback //action管理器
{
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>(); //将执行的动作的字典集合,int为key,SSAction为value
private List<SSAction> waitingAdd = new List<SSAction>(); //等待去执行的动作列表
private List<int> waitingDelete = new List<int>(); //等待删除的动作的key
protected void Update()
{
foreach (SSAction ac in waitingAdd)
{
actions[ac.GetInstanceID()] = ac; //获取动作实例的ID作为key
}
waitingAdd.Clear();
foreach (KeyValuePair<int, SSAction> kv in actions)
{
SSAction ac = kv.Value;
if (ac.destroy)
{
waitingDelete.Add(ac.GetInstanceID());
}
else if (ac.enable)
{
ac.Update();
}
}
foreach (int key in waitingDelete)
{
SSAction ac = actions[key];
actions.Remove(key);
DestroyObject(ac);
}
waitingDelete.Clear();
}
public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager)
{
action.gameobject = gameobject;
action.transform = gameobject.transform;
action.callback = manager;
waitingAdd.Add(action);
action.Start();
}
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null)
{
}
}
public class FlyActionManager : SSActionManager //本游戏管理器
{
private UFOFlyAction fly; //飞行动作,这次只有单独动作,没有组合动作
public FirstController sceneController;
protected void Start()
{
sceneController = (FirstController)SSDirector.GetInstance().CurrentScenceController;
sceneController.actionManager = this;
}
public void UFOFly(GameObject disk, float angle)
{
fly = UFOFlyAction.GetSSAction(disk.GetComponent<DiskData>().direction, angle, disk.GetComponent<DiskData>().speed);
this.RunAction(disk, fly, this);
}
public void Pause()
{
fly.run = false;
}
public void Run()
{
fly.run = true;
}
}
SSDirector.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSDirector : System.Object
{
//singlton instance
private static SSDirector _instance;
public ISceneController CurrentScenceController { get; set; }
//get instance
public static SSDirector GetInstance()
{
if (_instance == null)
{
_instance = new SSDirector();
}
return _instance;
}
}
ScoreRecorder.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreRecorder : MonoBehaviour
{
public int score; //分数
void Start()
{
score = 0;
}
//记录分数
public void Record(GameObject disk)
{
score = disk.GetComponent<DiskData>().score + score;
}
public void Miss()
{
if (score >= 2)
score -= 2;
else
score = 0;
}
//重置分数
public void Reset()
{
score = 0;
}
}