题目
编写一个简单的鼠标打飞碟(Hit UFO)游戏
游戏内容要求:
- 游戏有 n 个 round,每个 round 都包括10 次 trial;
- 每个 trial的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
- 每个 trial的飞碟有随机性,总体难度随 round 上升;
游戏的要求:
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
- 近可能使用前面MVC 结构实现人机交互与游戏模型分离
- 如果你的使用工厂有疑问,参考:弹药和敌人:减少,重用和再利用
实践内容
游戏架构
RoundController:游戏的导演,总控制器,其中的shoot负责检查是否击中飞碟
RoundActionManager:动作管理者,负责管理动作的产生
UserGUI:负责渲染整个页面的布局,主要是功能按钮的实现
ScoreRecorder:负责分数的计算,根据飞碟的大小,速度,颜色,计算打中的得分
DiskDate:挂在飞碟预制上的组件,规定了飞碟的属性
DiskFactory:负责生产不同大小,速度,颜色的飞碟
具体的源码如下:
- RoundController
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum State { WIN, LOSE, PAUSE, CONTINUE, START };
public interface ISceneController
{
State state { get; set; }
void LoadResources();
void Pause();
void Resume();
void Restart();
}
public class RoundController : MonoBehaviour, IUserAction, ISceneController
{
public DiskFactory diskFactory;
public RoundActionManager actionManager;
public ScoreRecorder scoreRecorder;
private List<GameObject> disks;
private int round;//第几个回合
private GameObject shootAtSth;
GameObject explosion;
//游戏状态
public State state { get; set; }
//计时器, 每关60秒倒计时
public int leaveSeconds;
//用来计数,每秒自动发射一次飞碟
public int count;
IEnumerator DoCountDown()
{
while (leaveSeconds >= 0)
{
yield return new WaitForSeconds(1);
leaveSeconds--;
}
}
void Awake()
{
SSDirector director = SSDirector.getInstance();
director.setFPS(60);
director.currentScenceController = this;
LoadResources();
diskFactory = Singleton<DiskFactory>.Instance;
scoreRecorder = Singleton<ScoreRecorder>.Instance;
actionManager = Singleton<RoundActionManager>.Instance;
leaveSeconds = 60;
count = leaveSeconds;
state = State.PAUSE;
disks = new List<GameObject>();
}
void Start()
{
round = 1;//从第一关开始
LoadResources();
}
void Update()
{
LaunchDisk();
Judge();
RecycleDisk();
}
public void LoadResources()
{
Camera.main.transform.position = new Vector3(0, 0, -30);
//explosion = Instantiate(Resources.Load("Prefabs/ParticleSys"), new Vector3(-40, 0, 0), Quaternion.identity) as GameObject;
}
public void shoot()//用户在游戏状态为开始或者继续时,才能左键射击
{
if (Input.GetMouseButtonDown(0) && (state == State.START || state == State.CONTINUE))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
if ((SSDirector.getInstance().currentScenceController.state == State.START || SSDirector.getInstance().currentScenceController.state == State.CONTINUE))
{
shootAtSth = hit.transform.gameObject;
//explosion.transform.position = hit.collider.gameObject.transform.position;
//explosion.GetComponent<Renderer>().material = hit.collider.gameObject.GetComponent<Renderer>().material;
//explosion.GetComponent<ParticleSystem>().Play();
}
}
}
}
public void LaunchDisk()//每秒自动发射飞碟
{
if (count - leaveSeconds == 1)
{
count = leaveSeconds;
GameObject disk = diskFactory.GetDisk(round);//从飞碟工厂得到飞碟
Debug.Log(disk);
disks.Add(disk);//飞碟进入场景
actionManager.addRandomAction(disk);//让动作管理者设计轨迹
}
}
public void RecycleDisk()//检查需不需要回收飞碟
{
for (int i = 0; i < disks.Count; i++)
{
if (disks[i].transform.position.z < -18)
{
diskFactory.FreeDisk(disks[i]);//让飞碟工厂回收
disks.Remove(disks[i]);
}
}
}
public void Judge()//判断游戏状态,是否射中以及够不够分数进入下一回合
{
if (shootAtSth != null && shootAtSth.transform.tag == "Disk" && shootAtSth.activeInHierarchy)//射中飞碟
{
scoreRecorder.Record(shootAtSth);//计分
diskFactory.FreeDisk(shootAtSth);//回收飞碟
shootAtSth = null;//点击的物体重置为空,避免计分出错
}
if (scoreRecorder.getScore() > 500 * round)//每关500分才能进入下一关,重新倒数60秒
{
round++;
leaveSeconds = count = 60;
}
if (round == 3) //只设计了两关, 所以赢了
{
StopAllCoroutines();
state = State.WIN;
}
else if (leaveSeconds == 0 && scoreRecorder.getScore() < 500 * round) //时间到,分数不够,输了
{
StopAllCoroutines();
state = State.LOSE;
}
else
state = State.CONTINUE;
}
public void Pause()
{
state = State.PAUSE;
StopAllCoroutines();
for (int i = 0; i < disks.Count; i++)
{
disks[i].SetActive(false);//暂停后飞碟不可见
}
}
public void Resume()
{
StartCoroutine(DoCountDown()); //开启协程计时
state = State.CONTINUE;
for (int i = 0; i < disks.Count; i++)
{
disks[i].SetActive(true);//恢复后飞碟可见
}
}
public void Restart()
{
scoreRecorder.Reset();
Application.LoadLevel(Application.loadedLevelName);
SSDirector.getInstance().currentScenceController.state = State.START;
}
}
- RoundActionManager
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISSActionCallback
{
void actionDone(SSAction source);
}
public class SSAction : ScriptableObject
{
public bool enable = true;
public bool destroy = false;
public GameObject gameObject { get; set; }
public Transform transform { get; set; }
public ISSActionCallback callback { get; set; }
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
public class MoveToAction : SSAction
{
public Vector3 target;
public float speed;
private MoveToAction() { }
public static MoveToAction getAction(Vector3 target, float speed)
{
MoveToAction action = ScriptableObject.CreateInstance<MoveToAction>();
action.target = target;
action.speed = speed;
return action;
}
public override void Update()
{
this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed * Time.deltaTime);
if (this.transform.position == target)
{
this.destroy = true;
this.callback.actionDone(this);
}
}
public override void Start() { }
}
public class SequenceAction : SSAction, ISSActionCallback
{
public List<SSAction> sequence;
public int repeat = -1; //-1表示无限循环,0表示只执行一遍,repeat> 0 表示重复repeat遍
public int currentAction = 0;//当前动作列表里,执行到的动作序号
public static SequenceAction getAction(int repeat, int currentActionIndex, List<SSAction> sequence)
{
SequenceAction action = ScriptableObject.CreateInstance<SequenceAction>();
action.sequence = sequence;
action.repeat = repeat;
action.currentAction = currentActionIndex;
return action;
}
public override void Update()
{
if (sequence.Count == 0) return;
if (currentAction < sequence.Count)
{
sequence[currentAction].Update();
}
}
public void actionDone(SSAction source)
{
source.destroy = false;
this.currentAction++;
if (this.currentAction >= sequence.Count)
{
this.currentAction = 0;
if (repeat > 0) repeat--;
if (repeat == 0)
{
this.destroy = true;
this.callback.actionDone(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()
{
foreach (SSAction action in sequence)
{
DestroyObject(action);
}
}
}
public class SSActionManager : MonoBehaviour
{
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
private List<SSAction> waitingToAdd = new List<SSAction>();
private List<int> watingToDelete = new List<int>();
protected void Update()
{
foreach (SSAction ac in waitingToAdd)
{
actions[ac.GetInstanceID()] = ac;
}
waitingToAdd.Clear();
foreach (KeyValuePair<int, SSAction> kv in actions)
{
SSAction ac = kv.Value;
if (ac.destroy)
{
watingToDelete.Add(ac.GetInstanceID());
}
else if (ac.enable)
{
ac.Update();
}
}
foreach (int key in watingToDelete)
{
SSAction ac = actions[key];
actions.Remove(key);
DestroyObject(ac);
}
watingToDelete.Clear();
}
public void RunAction(GameObject gameObject, SSAction action, ISSActionCallback whoToNotify)
{
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.callback = whoToNotify;
waitingToAdd.Add(action);
action.Start();
}
}
public class RoundActionManager : SSActionManager, ISSActionCallback
{
public RoundController scene;
public MoveToAction action1, action2;
public SequenceAction saction;
float speed;
public void addRandomAction(GameObject gameObj)
{
int[] X = { -20, 20 };
int[] Y = { -5, 5 };
int[] Z = { -20, -20 };
// 随机生成起始点和终点
Vector3 starttPos = new Vector3(
UnityEngine.Random.Range(-20, 20),
UnityEngine.Random.Range(-5, 5),
UnityEngine.Random.Range(50, 10)
);
gameObj.transform.position = starttPos;
Vector3 randomTarget = new Vector3(
X[UnityEngine.Random.Range(0, 2)],
Y[UnityEngine.Random.Range(0, 2)],
Z[UnityEngine.Random.Range(0, 2)]
);
MoveToAction action = MoveToAction.getAction(randomTarget, gameObj.GetComponent<DiskData>().speed);
RunAction(gameObj, action, this);
}
protected void Start()
{
scene = (RoundController)SSDirector.getInstance().currentScenceController;
scene.actionManager = this;
}
protected new void Update()
{
base.Update();
}
public void actionDone(SSAction source)
{
Debug.Log("Done");
}
}
- UserGUI
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface IUserAction
{
void shoot();//射击动作
}
public class UserGUI : MonoBehaviour
{
private IUserAction action;
private float width, height;
private string countDownTitle;
void Start()
{
countDownTitle = "Start";
action = SSDirector.getInstance().currentScenceController as IUserAction;
}
float castw(float scale)
{
return (Screen.width - width) / scale;
}
float casth(float scale)
{
return (Screen.height - height) / scale;
}
void OnGUI()
{
width = Screen.width / 12;
height = Screen.height / 12;
//倒计时
GUI.Label(new Rect(castw(2f) + 20, casth(6f) - 20, 50, 50), ((RoundController)SSDirector.getInstance().currentScenceController).leaveSeconds.ToString());
//分数
GUI.Button(new Rect(580, 10, 80, 30), ((RoundController)SSDirector.getInstance().currentScenceController).scoreRecorder.getScore().ToString());
if (SSDirector.getInstance().currentScenceController.state != State.WIN && SSDirector.getInstance().currentScenceController.state != State.LOSE
&& GUI.Button(new Rect(10, 10, 80, 30), countDownTitle))
{
if (countDownTitle == "Start")
{
//恢复场景
countDownTitle = "Pause";
SSDirector.getInstance().currentScenceController.Resume();
}
else
{
//暂停场景
countDownTitle = "Start";
SSDirector.getInstance().currentScenceController.Pause();
}
}
if (SSDirector.getInstance().currentScenceController.state == State.WIN)//胜利
{
if (GUI.Button(new Rect(castw(2f), casth(6f), width, height), "Win!"))
{
//选择重来
SSDirector.getInstance().currentScenceController.Restart();
}
}
else if (SSDirector.getInstance().currentScenceController.state == State.LOSE)//失败
{
if (GUI.Button(new Rect(castw(2f), casth(6f), width, height), "Lose!"))
{
SSDirector.getInstance().currentScenceController.Restart();
}
}
}
void Update()
{
//监测用户射击
action.shoot();
}
}
- ScoreRecorder
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreRecorder : MonoBehaviour
{
private float score;
public float getScore()
{
return score;
}
public void Record(GameObject disk)
{
//size越小、速度越快,分越高
score += (100 - disk.GetComponent<DiskData>().size * (20 - disk.GetComponent<DiskData>().speed));
//根据颜色加分
Color c = disk.GetComponent<DiskData>().color;
switch (c.ToString())
{
case "red":
score += 50;
break;
case "green":
score += 40;
break;
case "blue":
score += 30;
break;
case "yellow":
score += 10;
break;
}
}
public void Reset()
{
score = 0;
}
}
- DiskDate
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskData : MonoBehaviour
{
public float size;
public Color color;
public float speed;
}
- DiskFactory
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskFactory : MonoBehaviour
{
private List<GameObject> used = new List<GameObject>();//存储正在使用的飞碟
private List<GameObject> free = new List<GameObject>();//存储使用完了被回收的飞碟
//颜色数组用于随机分配颜色
private Color[] color = { Color.red, Color.green, Color.blue, Color.yellow };
//生产飞碟,先从回收部分取,若回收的部分为空,才从资源加载新的飞碟
public GameObject GetDisk(int ruler)
{
GameObject a_disk;
if (free.Count > 0)
{
a_disk = free[0];
free.Remove(free[0]);
}
else
{
a_disk = GameObject.Instantiate(Resources.Load("Prefabs/Disk")) as GameObject;
Debug.Log(a_disk);
}
switch (ruler)
{
case 1:
a_disk.GetComponent<DiskData>().size = UnityEngine.Random.Range(0, 6);//随机大小
a_disk.GetComponent<DiskData>().color = color[UnityEngine.Random.Range(0, 4)];//随机颜色
a_disk.GetComponent<DiskData>().speed = UnityEngine.Random.Range(10, 15);//不同关卡速度不同,同一关卡速度在一定范围内
a_disk.transform.localScale = new Vector3(a_disk.GetComponent<DiskData>().size * 2, a_disk.GetComponent<DiskData>().size * 0.1f, a_disk.GetComponent<DiskData>().size * 2);
a_disk.GetComponent<Renderer>().material.color = a_disk.GetComponent<DiskData>().color;
break;
case 2:
a_disk.GetComponent<DiskData>().size = UnityEngine.Random.Range(0, 4);
a_disk.GetComponent<DiskData>().color = color[UnityEngine.Random.Range(0, 4)];
a_disk.GetComponent<DiskData>().speed = UnityEngine.Random.Range(15, 20);
a_disk.transform.localScale = new Vector3(a_disk.GetComponent<DiskData>().size * 2, a_disk.GetComponent<DiskData>().size * 0.1f, a_disk.GetComponent<DiskData>().size * 2);
a_disk.GetComponent<Renderer>().material.color = a_disk.GetComponent<DiskData>().color;
break;
}
a_disk.SetActive(true);
used.Add(a_disk);
return a_disk;
}
//回收飞碟
public void FreeDisk(GameObject disk)
{
for (int i = 0; i < used.Count; i++)
{
if (used[i] == disk)
{
disk.SetActive(false);
used.Remove(used[i]);
free.Add(disk);
}
}
}
}
难点说明
- 工厂模式:主要是为了节省内存,提高效率,因此每次加载预制,在被击中后不是销毁,而且用一个链表储存起来,在生产新的飞碟时,判断该链表中是否存在可以用的飞碟,如果有则直接用,如果无才重新加载一个新的预制
- 另外一个是利用射线组件,来实现点击打中目标,具体实现代码在RoundController文件中的shoot函数里面