3D游戏设计与分析-打飞碟
目录
此次作业是要编写一个简单的鼠标打飞碟(Hit UFO)游戏
游戏内容要求
- 游戏有 n 个 round,每个 round 都包括10 次 trial;
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
游戏的要求
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
- 近可能使用前面 MVC 结构实现人机交互与游戏模型分离
各部分代码解释
Singleton.cs
/**
* 这是一个实现单例模式的模板,所有的MonoBehaviour对象都用这个模板来实现单实例
*/
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;
}
}
}
场景单实例类,当所需的实例第一次被需要时,在场景内搜索该实例,下一次使用时不需要搜索直接返回。
DiskObj.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskObj : MonoBehaviour {
public Vector3 size;
public Color color;
public float speed;
public Vector3 direction;
}
飞碟数据,携带飞碟的尺寸、颜色、飞行速度以及飞行方向。
DiskFactory.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskFactory : MonoBehaviour {
public GameObject diskPrefab;
private List<DiskObj> used = new List<DiskObj>();
private List<DiskObj> free = new List<DiskObj>();
private void Awake()
{
diskPrefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/disk"), Vector3.zero, Quaternion.identity);
diskPrefab.SetActive(false);
}
public GameObject GetDisk(int round)
{
GameObject newDisk = null;
if (free.Count > 0)
{
newDisk = free[0].gameObject;
free.Remove(free[0]);
}
else
{
newDisk = GameObject.Instantiate<GameObject>(diskPrefab, Vector3.zero, Quaternion.identity);
newDisk.AddComponent<DiskObj>();
}
int start = 0;
if (round == 1) start = 100;
if (round == 2) start = 250;
int selectedColor = Random.Range(start, round * 499);
if (selectedColor > 500)
{
round = 2;
}
else if (selectedColor > 300)
{
round = 1;
}
else
{
round = 0;
}
switch (round)
{
case 0:
{
newDisk.GetComponent<DiskObj>().color = Color.yellow;
newDisk.GetComponent<DiskObj>().speed = 4.0f;
float RanX = UnityEngine.Random.Range(-1f, 1f) < 0 ? -1 : 1;
newDisk.GetComponent<DiskObj>().direction = new Vector3(RanX, 1, 0);
newDisk.GetComponent<Renderer>().material.color = Color.yellow;
break;
}
case 1:
{
newDisk.GetComponent<DiskObj>().color = Color.red;
newDisk.GetComponent<DiskObj>().speed = 6.0f;
float RanX = UnityEngine.Random.Range(-1f, 1f) < 0 ? -1 : 1;
newDisk.GetComponent<DiskObj>().direction = new Vector3(RanX, 1, 0);
newDisk.GetComponent<Renderer>().material.color = Color.red;
break;
}
case 2:
{
newDisk.GetComponent<DiskObj>().color = Color.green;
newDisk.GetComponent<DiskObj>().speed = 8.0f;
float RanX = UnityEngine.Random.Range(-1f, 1f) < 0 ? -1 : 1;
newDisk.GetComponent<DiskObj>().direction = new Vector3(RanX, 1, 0);
newDisk.GetComponent<Renderer>().material.color = Color.black;
break;
}
}
used.Add(newDisk.GetComponent<DiskObj>());
//newDisk.SetActive(true);
newDisk.name = newDisk.GetInstanceID().ToString();
return newDisk;
}
public void FreeDisk(GameObject disk)
{
DiskObj tmp = null;
foreach (DiskObj i in used)
{
if (disk.GetInstanceID() == i.gameObject.GetInstanceID())
{
tmp = i;
}
}
if (tmp != null) {
tmp.gameObject.SetActive(false);
free.Add(tmp);
used.Remove(tmp);
}
}
}
飞碟工厂,负责生产与释放飞碟。
GetDisk:生产飞碟,首先从free空闲队列中查找是否有可用的飞碟,如果没有则新建一个飞碟。
FreeDisk:释放飞碟,将飞碟从used队列中移除并添加到free队列中。
SSAction.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSAction : ScriptableObject
{
public bool enable = false;
public bool destroy = false;
public GameObject gameobject { get; set; }
public Transform transform { get; set; }
public ISSActionCallback callback { get; set; }
protected SSAction() { }
public virtual void Start()
{
throw new System.NotImplementedException();
}
// Update is called once per frame
public virtual void Update()
{
throw new System.NotImplementedException();
}
public void reset()
{
enable = false;
destroy = false;
gameobject = null;
transform = null;
callback = null;
}
}
动作基类。
CCFlyAction.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCFlyAction : SSAction
{
/**
* acceleration是重力加速度,为9.8
*/
float acceleration;
/**
* horizontalSpeed是飞碟水平方向的速度
*/
float horizontalSpeed;
/**
* direction是飞碟的初始飞行方向
*/
Vector3 direction;
/**
* time是飞碟已经飞行的时间
*/
float time;
public override void Start()
{
enable = true;
acceleration = 9.8f;
time = 0;
horizontalSpeed = gameobject.GetComponent<DiskObj>().speed;
direction = gameobject.GetComponent<DiskObj>().direction;
}
// Update is called once per frame
public override void Update()
{
if (gameobject.activeSelf)
{
/**
* 计算飞碟的累计飞行时间
*/
time += Time.deltaTime;
/**
* 飞碟在竖直方向的运动
*/
transform.Translate(Vector3.down * acceleration * time * Time.deltaTime);
/**
* 飞碟在水平方向的运动
*/
transform.Translate(direction * horizontalSpeed * Time.deltaTime);
/**
* 当飞碟的y坐标比-4小时,飞碟落地
*/
if (this.transform.position.y < -4)
{
this.destroy = true;
this.enable = false;
this.callback.SSActionEvent(this);
}
}
}
public static CCFlyAction GetSSAction()
{
CCFlyAction action = ScriptableObject.CreateInstance<CCFlyAction>();
return action;
}
}
飞行动作。
SSActionManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSActionManager : MonoBehaviour {
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>(); //保存所以已经注册的动作
private List<SSAction> waitingAdd = new List<SSAction>(); //动作的等待队列,在这个对象保存的动作会稍后注册到动作管理器里
private List<int> waitingDelete = new List<int>(); //动作的删除队列,在这个对象保存的动作会稍后删除
// Update is called once per frame
protected void Update()
{
//把等待队列里所有的动作注册到动作管理器里
foreach (SSAction ac in waitingAdd) actions[ac.GetInstanceID()] = ac;
waitingAdd.Clear();
//管理所有的动作,如果动作被标志为删除,则把它加入删除队列,被标志为激活,则调用其对应的Update函数
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 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);
}
动作管理者的基类
CCActionManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCActionManager : SSActionManager, ISSActionCallback
{
public FirstSceneControl sceneController;
public List<CCFlyAction> Fly;
public int DiskNumber = 0;
/**
* used是用来保存正在使用的动作
* free是用来保存还未被激活的动作
*/
private List<SSAction> used = new List<SSAction>();
private List<SSAction> free = new List<SSAction>();
/**
* GetSSAction这个函数是用来获取CCFlyAction这个动作的,
* 每次首次判断free那里还有没有未使用的CCFlyActon这个动作,
* 有就从free那里获取,没有就生成一个CCFlyAction
*/
SSAction GetSSAction()
{
SSAction action = null;
if (free.Count > 0)
{
action = free[0];
free.Remove(free[0]);
}
else
{
action = ScriptableObject.Instantiate<CCFlyAction>(Fly[0]);
}
used.Add(action);
return action;
}
public void FreeSSAction(SSAction action)
{
SSAction tmp = null;
foreach (SSAction i in used)
{
if (action.GetInstanceID() == i.GetInstanceID())
{
tmp = i;
}
}
if (tmp != null)
{
tmp.reset();
free.Add(tmp);
used.Remove(tmp);
}
}
protected void Start()
{
sceneController = (FirstSceneControl)Director.getInstance().currentSceneControl;
sceneController.actionManager = this;
Fly.Add(CCFlyAction.GetSSAction());
}
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0,
string strParam = null,
UnityEngine.Object objectParam = null)
{
if (source is CCFlyAction)
{
DiskNumber--;
DiskFactory df = Singleton<DiskFactory>.Instance;
df.FreeDisk(source.gameobject);
FreeSSAction(source);
}
}
public void StartThrow(Queue<GameObject> diskQueue)
{
foreach (GameObject tmp in diskQueue)
{
RunAction(tmp, GetSSAction(), (ISSActionCallback)this);
}
}
}
飞行动作管理者
Director.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Director : System.Object {
public ISceneControl currentSceneControl { get; set; }
/**
* Director这个类是采用单例模式
*/
private static Director director;
private Director()
{
}
public static Director getInstance()
{
if (director == null)
{
director = new Director();
}
return director;
}
}
导演类。
FirstSceneController.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneControl
{
void LoadResources();
}
public class FirstSceneControl : MonoBehaviour ,ISceneControl , IUserAction
{
public CCActionManager actionManager { get; set; }
public ScoreRecorder scoreRecorder { get; set; }
public Queue<GameObject> diskQueue = new Queue<GameObject>();
private int diskNumber;
private int currentRound = -1;
public int round = 3;
private float time = 0;
private GameState gameState = GameState.START;
void Awake()
{
Director director = Director.getInstance();
director.currentSceneControl = this;
diskNumber = 10;
this.gameObject.AddComponent<ScoreRecorder>();
this.gameObject.AddComponent<DiskFactory>();
scoreRecorder = Singleton<ScoreRecorder>.Instance;
//director.currentSceneControl.LoadResources();
}
private void Update()
{
if (actionManager.DiskNumber == 0 && gameState == GameState.RUNNING)
{
gameState = GameState.ROUND_FINISH;
}
if (actionManager.DiskNumber == 0 && gameState == GameState.ROUND_START)
{
currentRound = (currentRound + 1) % round;
NextRound();
actionManager.DiskNumber = 10;
gameState = GameState.RUNNING;
}
if (time > 1)
{
ThrowDisk();
time = 0;
}
else
{
time += Time.deltaTime;
}
}
private void NextRound()
{
DiskFactory df = Singleton<DiskFactory>.Instance;
for (int i = 0; i < diskNumber; i++)
{
diskQueue.Enqueue(df.GetDisk(currentRound));
}
actionManager.StartThrow(diskQueue);
}
void ThrowDisk()
{
if (diskQueue.Count != 0)
{
GameObject disk = diskQueue.Dequeue();
/**
* 以下几句代码是随机确定飞碟出现的位置
*/
Vector3 position = new Vector3(0, 0, 0);
float y = UnityEngine.Random.Range(0f, 4f);
position = new Vector3(-disk.GetComponent<DiskObj>().direction.x * 7, y, 0);
disk.transform.position = position;
disk.SetActive(true);
}
}
public void LoadResources()
{
//DiskFactory df = Singleton<DiskFactory>.Instance;
//df.init(diskNumber);
//GameObject greensward = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/greensward"));
}
public void GameOver()
{
GUI.color = Color.red;
GUI.Label(new Rect(700, 300, 400, 400), "GAMEOVER");
}
public int GetScore()
{
return scoreRecorder.score;
}
public GameState getGameState()
{
return gameState;
}
public void setGameState(GameState gs)
{
gameState = gs;
}
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<DiskObj>() != null)
{
scoreRecorder.Record(hit.collider.gameObject);
/**
* 如果飞碟被击中,那么就移到地面之下,由工厂负责回收
*/
hit.collider.gameObject.transform.position = new Vector3(0, -5, 0);
}
}
}
}
场景控制器,负责游戏主要逻辑。
ScoreRecorder.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreRecorder : MonoBehaviour
{
/**
* score是玩家得到的总分
*/
public int score;
/**
* scoreTable是一个得分的规则表,每种飞碟的颜色对应着一个分数
*/
private Dictionary<Color, int> scoreTable = new Dictionary<Color, int>();
// Use this for initialization
void Start()
{
score = 0;
scoreTable.Add(Color.yellow, 10);
scoreTable.Add(Color.red, 20);
scoreTable.Add(Color.green, 40);
}
public void Record(GameObject disk)
{
score += scoreTable[disk.GetComponent<DiskObj>().color];
}
public void Reset()
{
score = 0;
}
}
记录分数。
UserGUI.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum GameState { ROUND_START, ROUND_FINISH, RUNNING, PAUSE, START }
public interface IUserAction
{
void GameOver();
GameState getGameState();
void setGameState(GameState gs);
int GetScore();
void hit(Vector3 pos);
}
public class UserGUI : MonoBehaviour
{
private IUserAction action;
bool isFirst = true;
// Use this for initialization
void Start()
{
action = Director.getInstance().currentSceneControl as IUserAction;
}
private void OnGUI()
{
if (Input.GetButtonDown("Fire1"))
{
Vector3 pos = Input.mousePosition;
action.hit(pos);
}
GUIStyle myStyle = new GUIStyle();
myStyle.fontSize = 30;
GUI.Label(new Rect(710, 5, 400, 400), "Score:" + action.GetScore().ToString(), myStyle);
if (isFirst && GUI.Button(new Rect(700, 100, 90, 90), "Start"))
{
isFirst = false;
action.setGameState(GameState.ROUND_START);
}
if (!isFirst && action.getGameState() == GameState.ROUND_FINISH && GUI.Button(new Rect(700, 100, 90, 90), "Next Round"))
{
action.setGameState(GameState.ROUND_START);
}
}
}
界面类,构建UI并捕捉用户动作。