文章目录
游戏要求
游戏内容要求:
- 游戏有 n 个 round,每个 round 都包括10 次 trial;
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
自定义规则(游戏制作思路)
- 本次游戏设置了3个round,虽然round比较少,但是难度递增明显,而且有一定运气成分,也就是说即使到了最后一个round,也有可能出现比较简单的trial。
- 游戏难度主要由飞碟飞行速度(不同颜色代表不同属性),飞碟同时出现个数决定,大致由round控制,但是带有随机性。
- 每个trial之间间隔1.5秒左右,且不带提示信息。
- 飞碟得分分别为1分、2分、3分对应3种速度的飞碟。
游戏制作代码
重复用到了之前实验时候的一些类,这里就不再列出代码了。
- 导演类,跟之前一样负责管理场景控制器,是单实例
- 动作基类、动作管理器基类,跟之前一样,只是子类的实现方式不同。
用户交互接口
public interface Interaction
{
void hit(Vector3 pos);
int GetScore();
int getState();
void changeState(int a);
void reset();
}
只要用于用户点击的时候判断碰撞,记录并显示分数、记录状态、改变状态(根据round、trial、漏掉的飞碟数等等决定),重置。这个需要场景控制器实现。
单例模板
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;
}
}
}
这个类可以用来实现各个类型的单例模式,使得每个类型有且仅有一个实例,与Director类似,不同的是这个类具有通用性,可以用于其他类型,只需更改模板的类型并且使用Instance的get函数来获取实例。
飞碟工厂
首先需要定义一个存储飞碟信息的结构,将其与飞碟实例绑定起来。
public class DiskInfo : MonoBehaviour {
public Vector3 pos; //初始位置
public Color color; //颜色,代表不同难度
public float speed; // 初速度
public Vector3 target; // 初速度方向
public int hit = 0; // 是否已经被击中一次,防止重复击中而多计分
}
下面就是工厂的类:
public class DiskFactory : MonoBehaviour
{
public GameObject disk = null;
private List<DiskInfo> activeList = new List<DiskInfo>();
private List<DiskInfo> freeList = new List<DiskInfo>();
int rand1 = 6, rand2 = 9, rand3 = 13;
public int difficulty = 0;
public GameObject GetDisk(int round)
{
GameObject newDisk = null;
if (freeList.Count > 0)
{
newDisk = freeList[0].gameObject;
freeList.Remove(freeList[0]);
}
else
{
newDisk = Instantiate(Resources.Load<GameObject>("Prefabs/Disk"), Vector3.zero, Quaternion.identity);
newDisk.AddComponent<DiskInfo>();
}
switch(round) {
case 1: {
difficulty = Random.Range(0, rand1);
break;
}
case 2: {
difficulty = Random.Range(0, rand2);
break;
}
case 3: {
difficulty = Random.Range(0, rand3);
break;
}
}
if (difficulty < rand1 - 1) {
newDisk.GetComponent<DiskInfo>().color = Color.white;
newDisk.GetComponent<DiskInfo>().speed = 5.0f;
float RanX = Random.Range(-1f, 1f) < 0 ? -2 : 2;
newDisk.GetComponent<DiskInfo>().target = new Vector3(RanX, 1, 0);
newDisk.GetComponent<Renderer>().material.color = Color.white;
}
else if (difficulty < rand2 - 1) {
newDisk.GetComponent<DiskInfo>().color = Color.red;
newDisk.GetComponent<DiskInfo>().speed = 7.0f;
float RanX = Random.Range(-1f, 1f) < 0 ? -2 : 2;
newDisk.GetComponent<DiskInfo>().target = new Vector3(RanX, 1, 0);
newDisk.GetComponent<Renderer>().material.color = Color.red;
}
else {
newDisk.GetComponent<DiskInfo>().color = Color.black;
newDisk.GetComponent<DiskInfo>().speed = 9.0f;
float RanX = Random.Range(-1f, 1f) < 0 ? -2 : 2;
newDisk.GetComponent<DiskInfo>().target = new Vector3(RanX, 1, 0);
newDisk.GetComponent<Renderer>().material.color = Color.black;
}
activeList.Add(newDisk.GetComponent<DiskInfo>());
return newDisk;
}
public void freeDisk(GameObject disk) {
DiskInfo tmp = null;
foreach (DiskInfo i in activeList)
{
if (disk.GetInstanceID() == i.gameObject.GetInstanceID())
{
tmp = i;
break;
}
}
if (tmp != null) {
tmp.gameObject.SetActive(false);
tmp.hit = 0;
freeList.Add(tmp);
activeList.Remove(tmp);
}
}
}
- 每次外界访问GetDisk函数的时候,都会传入参数round,来决定生产(或者重用)哪一个类型的飞碟,这个带有随机性,难度随round增大而增大。
- 需要维护两个飞碟实例队列,当飞碟被击中或者落地的时候,可以重复使用该飞碟实例,将其加入空闲队列,让其在下一回合有需要的时候以新的形式(改变位置或者颜色等)出现,重新回到使用队列。
- 根据随机产生的难度,改变飞碟实例的属性,并且将其加入使用队列中。如果空闲队列有对象,直接重用,否则新实例化一个(减少实例化带来的性能损耗)。
动作相关类
除了以前的动作基类和动作管理器基类之外,还需要新建飞碟运动的子类去继承
飞碟飞行类:
public class FlyAction : SSAction{
public float g = 9.8f;
public Vector3 to;//初速度方向
public float v; //初速度
public float v_down = 0;
public float time;
private FlyAction() {}
public static FlyAction GetSSAction(Vector3 target, float speed) {
FlyAction action = ScriptableObject.CreateInstance<FlyAction>();
action.to = target;
action.v = speed;
return action;
}
public override void Update() {
time += Time.fixedDeltaTime;
this.transform.position += Vector3.down * (float)(v_down*Time.fixedDeltaTime+0.5*g*(Time.fixedDeltaTime)*(Time.fixedDeltaTime));
this.transform.position += to * v * Time.fixedDeltaTime;
v_down = time * g;
if (this.transform.position.y <= -5) {
this.destroy = true;
if (this.transform.position.y > -15) {
Singleton<Judger>.Instance.Miss();
}
this.callBack.SSActionEvent(this);
}
}
public override void Start() {
}
}
- 需要实现GetSSAction的方法,便于外界获取动作对象。其中需要传入两个参数初速度和初速度方向,这就是飞行动作的关键(模拟重力的方向和大小是默认的)。
- 每次更新的时候都将物体移动一小段距离,这个移动根据两个方向运动进行叠加,模拟一个类抛物线运动的过程。
- 当运动到某个位置(摄像机视角以下看不见的位置,就停止运动,并回调),由于被击中后的飞碟也是设定到某个下方的位置,所以需要一个小小判断来区分飞碟是被击中还是自由落地的。
飞行动作管理器
public class FlyActionManager : ActionManager, ISSActionCallback
{
FlyAction UFOAction;
Controller controller;
private void Start()
{
controller = Director.getInstance().currentSceneController as Controller;
controller.actionManager = this;
}
public void flyUFO(GameObject disk, Vector3 target, float speed) {
UFOAction = FlyAction.GetSSAction(target, speed);
this.RunAction(disk, UFOAction, this);
}
public void SSActionEvent(SSAction action){
Singleton<DiskFactory>.Instance.freeDisk(action.gameObject);
}
}
- 这个类做的不多,就是使得创建一个飞行动作实例,并且将其绑定到特定的对象上,执行动作。
- 回调函数则是在运动完成后,利用飞碟工厂回收飞碟实例。
Controller场景控制器
public class Controller : MonoBehaviour, SceneController, ISSActionCallback, Interaction {
public FlyActionManager actionManager;
public DiskFactory diskFactory;
public Judger judger;
public UserUI ui;
public int trial = 10;
public float time = 0;
public int round = 1;
public int n = 3;
public int state = 0;
public Queue<GameObject> diskQueue = new Queue<GameObject>();
private void Awake()
{
Director director = Director.getInstance();
director.currentSceneController = this;
actionManager = gameObject.AddComponent<FlyActionManager>() as FlyActionManager;
this.gameObject.AddComponent<DiskFactory>();
this.gameObject.AddComponent<Judger>();
diskFactory = Singleton<DiskFactory>.Instance;
ui = gameObject.AddComponent<UserUI>() as UserUI;
judger = Singleton<Judger>.Instance;
}
private void Update()
{
if (state <= 0 || state == 2) {
return;
}
if (trial == 0 && round >= n) {
time += Time.deltaTime;
if (time > 3) {
changeState(-2);
time = 0;
}
return;
}
if (trial == 0 && state == 1)
{
state = 2;
}
if (trial == 0 && state == 3)
{
round = (round + 1);
if (round > n) {
return;
}
trial = 10;
state = 1;
}
if (time > 1.5)
{
if (Singleton<Judger>.Instance.checkGame() == false) {
changeState(-1);
return;
}
ThrowDisk();
time = 0;
}
else
{
time += Time.deltaTime;
}
}
public void ThrowDisk() {
int tmp = Random.Range(0, round);
int num = 0;
if (tmp < 0.9) {
diskQueue.Enqueue(diskFactory.GetDisk(round));
num = 1;
}
else if (tmp < 2) {
diskQueue.Enqueue(diskFactory.GetDisk(round));
diskQueue.Enqueue(diskFactory.GetDisk(round));
num = 2;
}
else
{
diskQueue.Enqueue(diskFactory.GetDisk(round));
diskQueue.Enqueue(diskFactory.GetDisk(round));
diskQueue.Enqueue(diskFactory.GetDisk(round));
num = 3;
}
for(int i = 0; i < num; i ++) {
GameObject disk = diskQueue.Dequeue();
Vector3 position = new Vector3(0, 0, 0);
float y = UnityEngine.Random.Range(-3f, 2f);
position = new Vector3(-disk.GetComponent<DiskInfo>().target.x * 7, y, 0);
disk.transform.position = position;
disk.SetActive(true);
actionManager.flyUFO(disk, disk.GetComponent<DiskInfo>().target,disk.GetComponent<DiskInfo>().speed);
}
trial --;
}
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<DiskInfo>() != null && hit.collider.gameObject.GetComponent<DiskInfo>().hit != 1)
{
hit.collider.gameObject.GetComponent<DiskInfo>().hit = 1;
judger.hit(hit.collider.gameObject);
hit.collider.gameObject.transform.position = new Vector3(0, -20, 0);
return;
}
}
}
public void loadResources() {
}
public void SSActionEvent(SSAction action) {
}
public int GetScore() {
return Singleton<Judger>.Instance.getScore();
}
//游戏结束
public int getState(){
return state;
}
//游戏重新开始
public void changeState(int a){
state = a;
}
public void reset() {
trial = 10;
round = 1;
time = 0;
}
}
这个类的代码比较多,稍微总结一下:
- 初始状态,将各个部件实例化并添加到自身
- Update状态,每一帧都判断当前状态(游戏结束或进行中,round和trial进行到哪一步)并作出相应状态变化。还有每隔一段事件就执行丢飞碟的函数,使得飞碟飞出。
- 丢飞碟函数,就是从工厂里获取实例对象(数量根据round随机),设置一下初始位置。然后执行它们的飞行动作。
- hit函数,只要处理用户鼠标点击的交互,判断上是否点击到了飞碟,需要与UI类协同。
- 状态分为几个:round结束,游戏结束(分为正常结束和中途死亡)、游戏运行中。不同状态下对应不同的状态转换。
裁判类
public class Judger : MonoBehaviour {
private int score;
private int miss;
void Start() {
score = 0;
miss = 0;
}
public int getScore() {
return score;
}
public bool checkGame() {
if (miss >= 10) {
return false;
}
return true;
}
public void hit(GameObject disk) {
if(disk.GetComponent<DiskInfo>().color == Color.white) {
score += 1;
}else if (disk.GetComponent<DiskInfo>().color == Color.red) {
score += 2;
}else {
score += 3;
}
}
public void Miss() {
Debug.Log("Miss one!");
miss += 1;
}
public void restart() {
miss = 0;
score = 0;
}
}
- 充当计分和判断输赢的角色,分别记录未打中飞碟数和分数
- 根据打中飞碟的颜色不同加不同分数
- 检查当前未击中数是否达到上限,宣布游戏结束
以上函数都需外界调用。根据不同情景来调用。
UI
public class UserUI : MonoBehaviour {
private Interaction action;
bool flag = true;
GUIStyle style1;
GUIStyle style2;
GUIStyle style3;
float time = 0;
void Start ()
{
action = Director.getInstance().currentSceneController as Interaction;
style1 = new GUIStyle("button");
style1.fontSize = 25;
style2 = new GUIStyle();
style2.fontSize = 35;
style2.alignment = TextAnchor.MiddleCenter;
style3 = new GUIStyle();
style3.fontSize = 25;
style3.alignment = TextAnchor.MiddleCenter;
}
private void OnGUI() {
if (action.getState() == -1) {
GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-105, 100, 50), "Game Over!", style2);
GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-55, 110, 40), "Your Score: "+Singleton<Judger>.Instance.getScore().ToString(), style3);
if (GUI.Button(new Rect(Screen.width/2-70, Screen.height/2, 150, 70), "Play again", style1)){
Singleton<Judger>.Instance.restart();
action.reset();
action.changeState(1);
}
return;
}else if (action.getState() == -2) {
GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-105, 100, 50), "Finished!", style2);
GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-55, 110, 40), "Your Score: "+Singleton<Judger>.Instance.getScore().ToString(), style3);
if (GUI.Button(new Rect(Screen.width/2-70, Screen.height/2, 150, 70), "Restart", style1)){
Singleton<Judger>.Instance.restart();
action.reset();
action.changeState(1);
}
return;
}
if (Input.GetButtonDown("Fire1"))
{
Vector3 pos = Input.mousePosition;
action.hit(pos);
}
GUI.Label(new Rect(5, 5, 100, 50), "Score: " +Singleton<Judger>.Instance.getScore().ToString(), style3);
if (flag) {
GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-95, 100, 50), "Hit UFO!", style2);
if(GUI.Button(new Rect(Screen.width/2-70, Screen.height/2, 150, 70), "Play", style1)) {
flag = false;
action.changeState(1);
}
}
if (!flag && action.getState() == 2)
{
GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-95, 100, 50), "Next Round!", style2);
time += Time.deltaTime;
if (time > 3.5) {
action.changeState(3);
time = 0;
}
}
}
}
获取Controller中的状态,并且对应显示不同的内容。
- round结束,游戏未结束时,显示NextRound字样
- 游戏刚开始(初始),显示游戏名字。判断是否按下开始按钮才开始游戏。
- 游戏结束(分为中途死亡和正常结束)显示对应的字样,并设置按钮使其重玩。重新开始同时重置裁判类和状态。
逻辑:
- 判断用户鼠标点击的位置,执行Controller的hit函数进一步判断是否击中目标。
游戏截图
实验到此结束!