编写一个简单的鼠标打飞碟(Hit UFO)游戏
内容要求
- 游戏有 n 个 round,每个 round 都包括10 次 trial;
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同,且都由该 round 的 ruler 控制;
- 每个 trial 的飞碟都有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同进行计算,规则可自由设定。
游戏要求
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂模式必须是场景单实例的(具体实现可参考资源 Singleton 模板类);
- 近可能使用前面的 MVC 结构实现人机交互与游戏模型的分离。
参考:
弹药和敌人:减少,重用和再利用,
Unity对象池(Object Pooling)理解与简单应用
实现
游戏规则
根据游戏的内容要求,可以有以下游戏规则:
- 每次游戏有3次 round,各个 round 都可积累下来分数,每个 round 都包括10 次 trial;
- 游戏中出现的飞碟的大小、颜色都相同,击中飞碟可获得1分,漏掉飞碟不扣分;
- 每个飞碟的发射位置、速度都不同;
- 游戏结束后可以选择重新开始,此时分数也会归零。
动作表(规则表)
动作 | 描述 |
---|---|
玩家点击飞碟 | 改变分数,被点击(击中)的飞碟消失 |
飞碟管理员产生新的飞碟 | 颜色、位置、速度按一定比例随机出现 |
记分员加分/减分 | 玩家击中不同的飞碟,会导致不同的加分/减分 |
玩家开始/重新开始 | 分数归零,重新开始游戏 |
相关概念与实现思路
参考课程网站:与游戏世界交互
工厂对象:
简单工厂又称为工厂方法,即类一个方法能够得到一个对象实例,使用者不需要知道该实例如何构建、初始化等细节。
- 游戏对象的创建与销毁高成本,必须减少销毁次数(如游戏中的子弹);
- 屏蔽创建与销毁的业务逻辑,使程序易于扩展;
- 在 Unity 中,【工厂方法 + 单实例 + 对象池】通常都是同时在一起使用的。
UML图:
设计师读图:
- 游戏由导演、场记、运动管理师、演员构成;
- 新游戏中,场记请了记分员、飞碟管理员;
- 飞碟管理员管理飞碟的发放与回收,自己有个小仓库管理这些飞碟;
- 记分员按飞碟的数据计分,记分员拥有计分规则;
- 场记只需要管理出飞碟规则与管理碰撞就可以了。
设计模式解读:
- DiskFactory 类是一个单实例类,用前面场景单实例创建;
- DiskFactory 类有工厂方法 GetDisk 产生飞碟,有回收方法 Free(Disk);
- DiskFactory 使用模板模式根据预制和规则制作飞碟;
- 对象模板包括飞碟对象与飞碟数据。
优势:
应对规则、地图等变化,让设计者最低代价管理规则变化。例如:
道具工厂通过场景单实例,构建了方便可取获取DISK的类,包装了复杂的 Disk 生产与回收逻辑,易于使用,包含 Disk 产生规则(控制每个 round 的难度),可以积极应对未来游戏规则的变化,减少维护成本;
记分员包装了计分规则(控制业务均衡),提供了简单的对外业务接口。
对象池的实现:(伪代码)
getDisk(ruler)
BEGIN
IF (free list has disk) THEN
a_disk = remove one from list
ELSE
a_disk = clone from Prefabs
ENDIF
Set DiskData of a_disk with the ruler
Add a_disk to used list
Return a_disk
END
FreeDisk(disk)
BEGIN
Find disk in used list
IF (not found) THEN THROW exception
Move disk from used to free list
END
实现与代码
制备预制体作为飞碟:
用 Sphere 和 Capsule 来制备飞碟,将 Sphere 命名为disk,并将 Capsule 作为其子对象,通过调整其缩放比例形成飞碟的形状。
代码:
DiskFactory.cs: 飞碟的工厂类,用于制造和销毁飞碟的工厂;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskFactory : MonoBehaviour {
public GameObject diskPrefab;
public List<DiskData> used = new List<DiskData>();
public List<DiskData> free = new List<DiskData>();
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 disk = null;
if(free.Count > 0)
{
disk = free[0].gameObject;
free.Remove(free[0]);
}
else
{
disk = GameObject.Instantiate<GameObject>(diskPrefab, Vector3.zero, Quaternion.identity);
disk.AddComponent<DiskData>();
}
int start;
switch (round)
{
case 0: start = 0; break;
case 1: start = 100; break;
default: start = 200; break;
}
int selectColor = Random.Range(start, round * 499);
round = selectColor / 250;
DiskData diskData = disk.GetComponent<DiskData>();
Renderer renderer = disk.GetComponent<Renderer>();
Renderer childRenderer = disk.transform.GetChild(0).GetComponent<Renderer>();
float ranX = Random.Range(-1, 1) < 0 ? -1.2f : 1.2f;
Vector3 direction = new Vector3(ranX, 1, 0);
switch (round)
{
case 0:
diskData.setDiskData(new Vector3(1.35f, 1.35f, 1.35f), Color.white, 4.0f, direction);
renderer.material.color = Color.white;
childRenderer.material.color = Color.white;
break;
case 1:
diskData.setDiskData(new Vector3(1f, 1f, 1f), Color.gray, 6.0f, direction);
renderer.material.color = Color.gray;
childRenderer.material.color = Color.gray;
break;
case 2:
diskData.setDiskData(new Vector3(0.7f, 0.7f, 0.7f), Color.black, 8.0f, direction);
renderer.material.color = Color.black;
childRenderer.material.color = Color.black;
break;
}
used.Add(diskData);
diskData.name = diskData.GetInstanceID().ToString();
disk.transform.localScale = diskData.getSize();
return disk;
}
public void freeDisk(GameObject disk)
{
DiskData temp = null;
foreach (DiskData i in used)
{
if (disk.GetInstanceID() == i.gameObject.GetInstanceID())
{
temp = i;
}
}
if (temp != null)
{
temp.gameObject.SetActive(false);
free.Add(temp);
used.Remove(temp);
}
}
}
Director.cs: 导演类,单例模式,继承System.Object,主要控制场景切换;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Director : System.Object {
public ISceneControl current { set; get; }
private static Director _Instance;
public static Director getInstance()
{
return _Instance ?? (_Instance = new Director());
}
}
ISceneController.cs: 接口场景类,负责指明具体实现的场景类要实现的方法,便于更多的类能通过接口来访问场景类,由 FirstSceneController 具体场景实现类来实现;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneControl {
void loadResources();
}
IUserAction.cs: 接口类,负责指明由用户行为引发的变化的方法,由 FirstSceneController 这个最高级的控制类来实现;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum GameState { ROUND_START, ROUND_FINISH, RUNNING, PAUSE, START, FUNISH}
public interface IUserAction{
GameState getGameState();
void setGameState(GameState gameState);
int getScore();
void hit(Vector3 pos);
}
DiskData.cs: 飞碟数据类,说明当前飞碟的状态,描述飞碟;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskData : MonoBehaviour {
private Vector3 size;
private Color color;
private float speed;
private Vector3 direction;
public DiskData() { }
public Vector3 getSize()
{
return size;
}
public float getSpeed()
{
return speed;
}
public Vector3 getDirection()
{
return direction;
}
public Color getColor()
{
return color;
}
public void setDiskData(Vector3 size, Color color, float speed, Vector3 direction)
{
this.size = size;
this.color = color;
this.speed = speed;
this.direction = direction;
}
}
FirstSceneController.cs: 场景控制,最高级的控制类,负责加载资源,底层数据与用户操作的GUI的交互,实现 ISceneControl 和 IUserAction;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstSceneControl : MonoBehaviour, ISceneControl, IUserAction {
public CCActionManager actionManager { set; get; }
public ScoreRecorder scoreRecorder { set; get; }
public Queue<GameObject> diskQueue = new Queue<GameObject>();
private int diskNumber = 0;
private int currentRound = -1;
private float time = 0;
private GameState gameState = GameState.START;
void Awake()
{
Director director = Director.getInstance();
director.current = this;
diskNumber = 10;
this.gameObject.AddComponent<ScoreRecorder>();
this.gameObject.AddComponent<DiskFactory>();
scoreRecorder = Singleton<ScoreRecorder>.Instance;
director.current.loadResources();
}
public void loadResources()
{}
private void Update()
{
if(actionManager.getDiskNumber() == 0 && gameState == GameState.RUNNING)
{
gameState = GameState.ROUND_FINISH;
if(currentRound == 2)
{
gameState = GameState.FUNISH;
return;
}
}
if(actionManager.getDiskNumber() == 0 && gameState == GameState.ROUND_START)
{
currentRound++;
nextRound();
actionManager.setDiskNumber(10);
gameState = GameState.RUNNING;
}
if(time > 1 && gameState != GameState.PAUSE)
{
throwDisk();
time = 0;
}
else
{
time += Time.deltaTime;
}
}
private void nextRound()
{
DiskFactory diskFactory = Singleton<DiskFactory>.Instance;
for(int i = 0; i < diskNumber; i++)
{
diskQueue.Enqueue(diskFactory.getDisk(currentRound));
}
actionManager.startThrow(diskQueue);
}
void throwDisk()
{
if(diskQueue.Count != 0)
{
GameObject disk = diskQueue.Dequeue();
Vector3 pos = new Vector3(-disk.GetComponent<DiskData>().getDirection().x * 10, Random.Range(0f, 4f), 0);
disk.transform.position = pos;
disk.SetActive(true);
}
}
public int getScore()
{
return scoreRecorder.score;
}
public GameState getGameState()
{
return gameState;
}
public void setGameState(GameState gameState)
{
this.gameState = gameState;
}
public void hit(Vector3 pos)
{
RaycastHit[] hits = Physics.RaycastAll(Camera.main.ScreenPointToRay(pos));
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, -5, 0);
}
}
}
}
UserGUI.cs: 场景控制,用户界面类,负责生成界面交于用户操作,显示按钮,分数等;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class UserGUI : MonoBehaviour {
private IUserAction action;
bool isFirst = true;
GUIStyle red;
GUIStyle black;
// Use this for initialization
void Start () {
action = Director.getInstance().current as IUserAction;
black = new GUIStyle("button");
black.fontSize = 20;
red = new GUIStyle();
red.fontSize = 30;
red.fontStyle = FontStyle.Bold;
red.normal.textColor = Color.red;
red.alignment = TextAnchor.UpperCenter;
}
private void OnGUI()
{
if (action.getGameState() == GameState.FUNISH)
{
GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height / 2 - 150, 200, 100), action.getScore() >= 30 ? "You win" : "You fail", red);
if(GUI.Button(new Rect(Screen.width / 2 - 60, Screen.height / 2 - 50, 120, 40), "Restart", black))
{
SceneManager.LoadScene("DiskAttack");
}
return;
}
Rect rect = new Rect(Screen.width / 2 - 100, 0, 200, 40);
Rect rect2 = new Rect(Screen.width / 2 - 45, 60, 120, 40);
if (Input.GetButtonDown("Fire1") && action.getGameState() != GameState.PAUSE)
{
Vector3 pos = Input.mousePosition;
action.hit(pos);
}
if (!isFirst)
{
GUI.Label(rect, "Your score: " + action.getScore().ToString(), red);
}
else
{
GUIStyle blackLabel = new GUIStyle();
blackLabel.fontSize = 16;
blackLabel.normal.textColor = Color.black;
GUI.Label(new Rect(Screen.width / 2 - 250, 120, 500, 200), "There are 3 rounds, every round has 10 disk " +
"whose color is different.\nIf you attack the white one, you will get 1 score. And you will get 2 score\n" +
"if you attack the gray one. Finally, if you can attack the black and most\nfast one, you will get 4 " +
"score. Once you get 30 scores, you win!", blackLabel);
}
if (action.getGameState() == GameState.RUNNING && GUI.Button(rect2, "Paused", black))
{
action.setGameState(GameState.PAUSE);
}
else if(action.getGameState() == GameState.PAUSE && GUI.Button(rect2, "Run", black))
{
action.setGameState(GameState.RUNNING);
}
if (isFirst && GUI.Button(rect2, "Start", black))
{
isFirst = false;
action.setGameState(GameState.ROUND_START);
}
if(!isFirst && action.getGameState() == GameState.ROUND_FINISH && GUI.Button(rect2, "Next Round", black))
{
action.setGameState(GameState.ROUND_START);
}
}
}
SSAction.cs: 动作管理,是所有动作的基础类,用于规定所有动作的基础规范,通过实现 SSAciton 来指定不同的动作,继承 ScriptableObject;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSAction : ScriptableObject {
public bool enable = false;
public bool destroy = false;
public GameObject gameObject { set; get; }
public Transform transform { set; get; }
public ISSActionCallback callback { set; get; }
protected SSAction() { }
public virtual void Start()
{
throw new System.NotImplementedException();
}
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
{
float acceleration;
float horizontalSpeed;
Vector3 direction;
float time;
public static CCFlyAction getCCFlyAction()
{
CCFlyAction action = ScriptableObject.CreateInstance<CCFlyAction>();
return action;
}
public override void Start()
{
enable = true;
acceleration = 9.8f;
time = 0;
horizontalSpeed = gameObject.GetComponent<DiskData>().getSpeed();
direction = gameObject.GetComponent<DiskData>().getDirection();
}
public override void Update()
{
if (gameObject.activeSelf)
{
time += Time.deltaTime;
transform.Translate(Vector3.down * acceleration * time * Time.deltaTime);
transform.Translate(direction * horizontalSpeed * Time.deltaTime);
if(this.transform.position.y < -4)
{
this.destroy = true;
this.enable = false;
this.callback.SSActionEvent(this);
}
}
}
}
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>();
protected void Update()
{
foreach (SSAction action in waitingAdd)
{
actions[action.GetInstanceID()] = action;
}
waitingAdd.Clear();
foreach(KeyValuePair<int, SSAction> i in actions)
{
SSAction value = i.Value;
if (value.destroy)
{
waitingDelete.Add(value.GetInstanceID());
}
else if (value.enable)
{
value.Update();
}
}
foreach(int i in waitingDelete)
{
SSAction ac = actions[i];
actions.Remove(i);
DestroyObject(ac);
}
}
public void runAction(GameObject gameObject, SSAction action, ISSActionCallback manager)
{
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.callback = manager;
waitingAdd.Add(action);
action.Start();
}
}
CCActionManager.cs: 动作管理器,事件管理类,对 SSManager 进行加强,负责事件的处理,管理某个对象的具体动作,为了减小开销,CCActionManager 也作为一个工厂来管理 CCFlyAction,继承了 SSActionManager,实现了 ISSActionCallback;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCActionManager : SSActionManager, ISSActionCallback {
private FirstSceneControl sceneControl;
private List<CCFlyAction> flys = new List<CCFlyAction>();
private int diskNumber = 0;
private List<SSAction> used = new List<SSAction>();
private List<SSAction> free = new List<SSAction>();
public void setDiskNumber(int dn)
{
diskNumber = dn;
}
public int getDiskNumber()
{
return diskNumber;
}
public SSAction getSSAction()
{
SSAction action = null;
if(free.Count > 0)
{
action = free[0];
free.Remove(free[0]);
}
else
{
action = ScriptableObject.Instantiate<CCFlyAction>(flys[0]);
}
used.Add(action);
return action;
}
public void freeSSAction(SSAction action)
{
foreach(SSAction a in used)
{
if(a.GetInstanceID() == action.GetInstanceID())
{
a.reset();
free.Add(a);
used.Remove(a);
break;
}
}
}
protected void Start()
{
sceneControl = (FirstSceneControl)Director.getInstance().current;
sceneControl.actionManager = this;
flys.Add(CCFlyAction.getCCFlyAction());
}
private new void Update()
{
if (sceneControl.getGameState() == GameState.RUNNING)
base.Update();
}
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Completed, int intPram = 0
, string strParm = null, Object objParm = null)
{
if(source is CCFlyAction)
{
diskNumber--;
Singleton<DiskFactory>.Instance.freeDisk(source.gameObject);
freeSSAction(source);
}
}
public void startThrow(Queue<GameObject> diskQueue)
{
foreach(GameObject i in diskQueue)
{
runAction(i, getSSAction(), (ISSActionCallback)this);
}
}
}
ISSActionCallback.cs: 动作管理,动作事件接口类,定义了事件处理的接口,必须被事件管理器实现,作为 ActionManager 和 Action 之间的通信,规定动作执行完之后需要执行的行为;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum SSActionEventType : int { Started, Completed}
public interface ISSActionCallback {
void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Completed, int intPram = 0
, string strParm = null, Object objParm = null);
}
ScoreRecorder.cs: 记分员,负责给用户计分;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreRecorder : MonoBehaviour {
public int score;
private Dictionary<Color, int> scoreTable = new Dictionary<Color, int>();
void Start()
{
score = 0;
scoreTable.Add(Color.white, 1);
scoreTable.Add(Color.gray, 2);
scoreTable.Add(Color.black, 4);
}
public void reset()
{
score = 0;
}
public void record(GameObject disk)
{
score += scoreTable[disk.GetComponent<DiskData>().getColor()];
}
}
Singleton.cs: Singleton模板类;
(单例模式:一种软件设计模式,其核心结构只包含一个被称为单例的特殊类。一个类只有一个对象实例,但如果经常被使用,Singleton模板类就可以通过使用泛型来为每一个需要使用单例模式的类创建一个且唯一的一个对象实例,从而减少繁多的代码,提升效率。)
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;
}
}
}
实现结果
编写一个简单的自定义 Component (选做)
用自定义组件定义几种飞碟,做成预制:
- 参考:官方脚本手册
- 实现自定义组件,编辑并赋予飞碟一些属性。