游戏内容要求
- 游戏有 n 个 round,每个 round 都包括10 次 trial;
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
游戏的要求
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类。
- 近可能使用前面 MVC 结构实现人机交互与游戏模型分离。
游戏实现
Model属性分析
飞碟本身有大小、颜色、形状、运动属性,其中运动属性又包括发射位置、速度、角度属性。
类工厂属性方法
类工厂负责提供飞碟实例,为了节省内存空间耗费,可以采用Pool的方法,保存两个实例队列,一个用于保存正在使用的实例,另一个保存实例化完成但没有使用的实例,当有需要提供一个飞碟实例时,调用GetOneDisk()
方法从未使用实例队列或者重新实例化一个Disk
实例。
关键在于对Disk
实例属性的赋值必须具有随机性。C#语言的System库
中有Random类
,可以提供随机数,通过对随机数的判断,即可实现属性赋值的随机性。
此外,考虑每一回合的难度不同,回合结束或者游戏重新开始,所以需要清空正在使用的实例队列中的所有实例。
Controller组件规划
基本上是由Director类
统筹整个游戏,由于需要使用单例模式,所以为导演类声明静态的实例化方法getInstance()
来确保同一时刻只有一个游戏运行。同时游戏的具体实现由场记类实现,所以在导演类中需要添加一个场记接口,为游戏场景运行提供支持。
场记类负责调度各种事件,实现场景的加载、事件的执行。
用户行为和用户行为管理类作为一套,UserAction类
负责实现各种可能的用户行为,定义行为对应的执行事件,ActionManager类
负责对行为的调度,实现行为。在本次项目中,
View视图规划
视图的设计时层次化的,考虑到游戏是回合制的,所以有几个游戏状态:游戏未开始,游戏开始,游戏中回合前,游戏中回合间,游戏中回合后。
针对不同状态需要进行状态转移,而且对象的实例化在不同回合需要也会不同。
因为添加了血量,红色的飞碟被击中将会减少血量,当血量为0游戏结束。所以需要在View中完成对血量的判断和显示。
针对不同的状态,视图必须不同。
动作管理类
动作管理类主要负责对各个动作的统筹实现,SSAction
和SSActionManager
基本结构具有普适性,只需要完成其子类的Update方法
,即可使场记类(controller)对场景的调度过程中根据事件执行Action
。
本次项目中主要是SSAction
的Fly方法
在Update
中使用Translate
对游戏对象施加运动,包括向下的重力加速度,即可实现抛物线运动。SSActionManager
的loadAction方法
调用RunAction
将实例对象与动作绑定,实现运动。
状态转移的判断
主要通过三个参数,isOver
确定游戏是否开始,inRound
确定在回合前、回合内、回合后间隔,actionManager.disk_number
确定回合内的状态,若等于零就代表所有飞碟都已抛出完毕,回合结束,当然也可能是游戏并未开始,所以需要与isRound==1
共同使用,是否能进入下一回合。
关键代码
DiskFactory.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Data;
//using System;
using DiskClass;
namespace DiskFactoryClass
{
public class DiskFactory : MonoBehaviour
{
public GameObject diskModel = null; /* 飞碟游戏对象 */
private List<Disk> used = new List<Disk>(); /* 类似pool,用于减少内存消耗 */
private List<Disk> free = new List<Disk>();
public GameObject GetOneDisk(int roundNow)
{
diskModel = null;
//System.Random randomNum = new System.Random(1);
//Debug.Log("DiskFactory:" + free.Count +" "+ used.Count);
if (free.Count > 0)
{
diskModel = free[0].gameObject;
free.Remove(free[0]);
}
else
{
int id = Random.Range(-10, 10);
if (id > roundNow/2)
{
//Debug.Log("white");
diskModel = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Plate1"), Vector3.zero, Quaternion.identity);
diskModel.AddComponent<Disk>(); /* 将GameObject加入Scene */
diskModel.GetComponent<Disk>().id = true;
}
else
{
//Debug.Log("red");
diskModel = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Plate2"), Vector3.zero, Quaternion.identity);
diskModel.AddComponent<Disk>();
diskModel.GetComponent<Disk>().id = false;
diskModel.GetComponent<Disk>().value = 1;
}
diskModel.GetComponent<Disk>().speed = Random.Range(0,5);
float runX = UnityEngine.Random.Range(-1F, 1F) < 0 ? -1 : 1;
//diskModel.GetComponent<Renderer>().material.color = diskModel.GetComponent<Disk>().color; /* 设置Scene中GameObject的显示颜色,Renderer是显示类 */
diskModel.GetComponent<Disk>().direction = new Vector3(runX, 1, 0);
}
used.Add(diskModel.GetComponent<Disk>());
diskModel.name = diskModel.GetInstanceID().ToString();
return diskModel;
}
public void FreeUsingDisk(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;
}
}
}
}
}
GameView.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UserActions;
using Director;
namespace GameView
{
public class pageImage : MonoBehaviour
{
IUserAction userAction;
public bool isOver = true;
// Start is called before the first frame update
void Start()
{
userAction = SSDirector.getInstance().currentSceneController as IUserAction;
}
private void OnGUI()
{
if (!isOver)//游戏正在进行中
{
/* 点击事件 */
if (Input.GetButtonDown("Fire1"))
{
Vector3 pos = Input.mousePosition;
userAction.click(pos);
}
/* 分数和生命值标识 */
GUI.color = Color.red;
GUI.Label(new Rect(2 * Screen.width / 3, 0, 400, 400), "Score: " + userAction.getScore().ToString());
GUI.Label(new Rect(Screen.width-120, 0, 400,400), "Life:" + userAction.getLife().ToString());
GUI.color = Color.red;
if (userAction.getLife() == 0)//游戏失败
{
GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height / 2 - 200, 400, 400), "GAMEOVER\n" + " your score is " + userAction.getScore().ToString());
if (GUI.Button(new Rect(Screen.width / 2 - 45, Screen.height / 2 - 45, 90, 90), "Restart"))
{
userAction.resetScore();
userAction.setLife();
//isOver = false;
}
}
else//游戏正常执行中,确定当前回合数
{
GUI.color = Color.red;
GUI.Label(new Rect(3 * Screen.width / 4, 0, 400, 400), "Round: " + (userAction.getRound() + 1).ToString());
if (userAction.isInRound()==2 && GUI.Button(new Rect(Screen.width / 2 - 45, Screen.height / 2 - 45, 90, 90), "Next Round"))//在回合间隔中,按钮进入下一回合
{
userAction.setRoundState(0);
}
}
}
else if (userAction.getRound() == 5)
{
GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height / 2 - 200, 400, 400), "YOU WIN!!!\n" + " your score is " + userAction.getScore().ToString());
if (GUI.Button(new Rect(Screen.width / 2 - 45, Screen.height / 2 - 45, 90, 90), "Restart"))
{
userAction.resetScore();
userAction.setLife();
userAction.setRound(-1);
//isOver = false;
}
}
else//游戏未开始
{
if(GUI.Button(new Rect(Screen.width / 2 - 45, Screen.height / 2 - 45, 90, 90), "Start"))
{
isOver = false;
userAction.setRoundState(0);
}
}
}
}
}
Controller.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UserActions;
using Director;
using DiskClass;
using GameView;
using DiskFactoryClass;
using ActionManager;
using SceneController;
using SingletonClass;
/* 场记类实现:需要继承场记接口 */
public class Controller : MonoBehaviour, ISceneController, IUserAction
{
public pageImage gameGUI;
public CCActionManager actionManager;
public Queue<GameObject> disks = new Queue<GameObject>();
public int score = 0;
private int diskNumber=5;
private int currentRound = -1;
public int life = 5;
public int finalRound = 5;
public int inRound = 0;
private float time = 0; //计时
private float roundTime = 1; //每个回合隔多久飞一次飞碟
/* 场记接口方法的实现,主要完成场景载入 */
public void LoadResource()
{
//无需载入初始场景
}
public int isInRound()
{
return inRound;
}
public void setRoundState(int state)
{
inRound = state;
}
public int getScore()
{
return score;
}
public void click(Vector3 pos)
{
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit[] clicks;
clicks = Physics.RaycastAll(ray);
for (int i = 0; i < clicks.Length; i++)
{
RaycastHit hit = clicks[i];
if (hit.collider.gameObject.GetComponent<Disk>() != null)
{
if (hit.collider.gameObject.GetComponent<Disk>().id)
{
score += hit.collider.gameObject.GetComponent<Disk>().value;
hit.collider.gameObject.transform.position = new Vector3(0, -10, 0);
}
else
{
life--;
hit.collider.gameObject.transform.position = new Vector3(0, -10, 0);
}
}
}
}
public int getRound()
{
return currentRound;
}
public void setRound(int num)
{
currentRound = num;
}
public void resetScore()
{
score = 0;
}
public int getLife()
{
return life;
}
public void setLife()
{
life = 5;
}
/* 控制器的生命周期 */
void Awake()
{
SSDirector director = SSDirector.getInstance();
director.currentSceneController = this;
director.currentSceneController.LoadResource();
}
void Start()
{
Debug.Log("First SceneController Start!");
gameGUI = gameObject.AddComponent<pageImage>() as pageImage;
actionManager = gameObject.AddComponent<CCActionManager>() as CCActionManager;
this.gameObject.AddComponent<DiskFactory>();
}
private void Update()
{
if (inRound==2 && currentRound==finalRound)
{
gameGUI.isOver = true;
}
else
{
if (actionManager.disk_number == 0 && inRound==1&&!gameGUI.isOver)
{
inRound = 2;
}
if (actionManager.disk_number == 0 && inRound==0&&!gameGUI.isOver)
{
currentRound++;
actionManager.disk_number = (currentRound + 1) * diskNumber;
Debug.Log("Controller's disk number:" + actionManager.disk_number);
inRound = 1;
}
Debug.Log(actionManager.disk_number);
if (time > roundTime)
{
if (actionManager.disk_number>0)
{
DiskFactory df = Singleton<DiskFactory>.Instance;
time = 0;
actionManager.loadAction(df.GetOneDisk(currentRound));
}
}
else
{
time += Time.deltaTime;
}
}
}
}