一、游戏介绍
- 游戏内容要求:
- 游戏有 n 个 round,每个 round 都包括10 次 trial;
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
- 游戏的要求:
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
- 尽可能使用前面 MVC 结构实现人机交互与游戏模型分离
- 按 adapter模式 设计图修改飞碟游戏
- 使它同时支持物理运动与运动学(变换)运动
针对上面的游戏要求,我给游戏设计了4个模式,规则如下:
- 正常模式共有五轮飞碟,飞碟数列遵循斐波那契数列,当所有飞碟发射完毕时游戏结束。
- 无限模式拥有无限轮数
- 红色飞碟为4分,黄色飞碟为3分,绿色飞碟为2分,蓝色飞碟为1分。
- 物理学模式下飞碟会互相碰撞
二、游戏准备
1、UML图
将工厂方法 + 单实例 + 对象池的UML图完成对飞碟的创建和回收,再用Adapter模式来给飞碟增加物理学运动,同时还需要保留之前的MVC架构,所以最后形成的UML图如下:
对UML图的理解:
- 游戏由导演、场记、运动管理师、演员构成。
- 新游戏中,场记请了记分员、飞碟管理员
- 飞碟管理员管理飞碟的发放与回收,自己有个小仓库管理这些飞碟
- 记分员按飞碟的数据计分,记分员拥有计分规则
- 场记只需要管理出飞碟规则与管理碰撞就可以了
- 新建 PhysisActionManager来形成Adapter模式形成物理学运动
2、伪代码
对象池的实现:
- DiskFactory 类是一个单实例类,用前面场景单实例创建
- DiskFactory 类有工厂方法 GetDisk 产生飞碟,有回收方法 Free(Disk)
- DiskFactory 使用模板模式根据预制和规则制作飞碟
- 对象模板包括飞碟对象与飞碟数据
伪代码如下:
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
三、游戏代码
为了在游戏中使用MVC结构并采用面向对象的变成,所以SSDirector类、SSAction类、SSActionManager类、ISceneController类和ISSActionCallback类均保持不变,其他类的代码都需要改变,改变的代码如下:
Singleton
运用模板,可以为每个 MonoBehaviour子类 创建一个对象的实例。在任意位置使用代码Singleton<YourMonoType>.Instance
获得该对象。
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{//运用模板,可以为每个 MonoBehaviour子类 创建一个对象的实例。
protected static T instance;
public static T Instance { //在任意位置使用代码 Singleton<YourMonoType>.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;
}
}
}
Disk
完成飞碟对象的创建
public class Disk : MonoBehaviour
{
public GameObject disk;
//public DiskData data;
public Disk(string name){//四种飞碟
if(name == "disk1"){
disk = GameObject.Instantiate(Resources.Load<GameObject>("disk1"), Vector3.zero, Quaternion.identity) as GameObject;
disk.AddComponent<DiskData>();
//data=disk.GetComponent<DiskData>();
disk.GetComponent<DiskData>().points=1;
disk.GetComponent<DiskData>().direction=new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
disk.GetComponent<DiskData>().speed=1.0f;
}
else if(name == "disk2"){
disk = GameObject.Instantiate(Resources.Load<GameObject>("disk2"), Vector3.zero, Quaternion.identity) as GameObject;
disk.AddComponent<DiskData>();
//data=disk.GetComponent<DiskData>();
disk.GetComponent<DiskData>().points=2;
disk.GetComponent<DiskData>().direction=new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
disk.GetComponent<DiskData>().speed=2.0f;
}
else if(name == "disk3"){
disk = GameObject.Instantiate(Resources.Load<GameObject>("disk3"), Vector3.zero, Quaternion.identity) as GameObject;
disk.AddComponent<DiskData>();
//data=disk.GetComponent<DiskData>();
disk.GetComponent<DiskData>().points=3;
disk.GetComponent<DiskData>().direction=new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
disk.GetComponent<DiskData>().speed=3.0f;
}
else if(name == "disk4"){
disk = GameObject.Instantiate(Resources.Load<GameObject>("disk4"), Vector3.zero, Quaternion.identity) as GameObject;
disk.AddComponent<DiskData>();
disk.GetComponent<DiskData>().points=4;
disk.GetComponent<DiskData>().direction=new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
disk.GetComponent<DiskData>().speed=4.0f;
}
//return disk;
}
public GameObject getGameObject(){return disk;}
}
DiskData
飞碟的数据,携带飞碟的飞行速度、得分、以及飞行方向。
public class DiskData: MonoBehaviour
{
public float speed;//飞行速度
public Vector3 direction;//初始飞行方向
public int points;//得分
}
DiskFactory
负责生产和释放飞碟
public class DiskFactory : MonoBehaviour
{
// Start is called before the first frame update
List<DiskData> used;
List<DiskData> free;
public void Start(){
used = new List<DiskData>();
free = new List<DiskData>();
}
public GameObject GetDisk(){
Debug.Log("get...\n");
Disk disk=null;
GameObject d;
int rand=Random.Range(0,99);//生成一个随机数
if(free.Count>0){//若还有空闲的飞碟,就将其加入
Debug.Log("free...\n");
d=free[0].gameObject;
//disk.getGameObject().SetActive(true);
free.Remove(free[0]);
}
else{
Debug.Log("new...\n");
if(rand%4==0){
disk=new Disk("disk1");
}
else if(rand%4==1){
disk=new Disk("disk2");
}
else if(rand%4==2){
disk=new Disk("disk3");
}
else{
disk=new Disk("disk4");
}
d=disk.getGameObject();
}
used.Add(d.GetComponent<DiskData>());
return d;
}
public void FreeDisk(GameObject disk){
for(int i=0;i<used.Count;i++){
if(disk.GetInstanceID() == used[i].gameObject.GetInstanceID()){
//used[i].getGameObject().SetActive(false);
disk.SetActive(false);
used.Remove(used[i]);
free.Add(used[i]);
break;
}
}
}
}
IUserAction
包含对运动学和物理学的飞行模式的切换,是否将游戏设置为无穷模式,控制飞碟的点击得分。
public interface IUserAction
{
void setFlyMode(bool isPhysis);//设置飞行模式
void Hit(Vector3 position);//点击
void Restart();//重置
void SetMode(bool isInfinite);//选择模式
bool Check();
int GetPoints();
}
FirstController
对接口的函数的实现
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstController : MonoBehaviour, ISceneController, IUserAction {
DiskFactory diskFactory;
RoundController roundController;
//UserGUI userGUI;
public IActionManager actionManager;
public int points;
void Awake () {
SSDirector director = SSDirector.getInstance ();
director.setFPS (60);
points=0;
director.currentSceneController = this;
LoadResources ();
}
// loading resources for first scence
public void LoadResources () {
UnityEngine.Debug.Log("load...\n");
//获得对象
diskFactory=Singleton<DiskFactory>.Instance;
roundController=Singleton<RoundController>.Instance;
}
public void Pause ()
{
throw new System.NotImplementedException ();
}
public void Resume ()
{
throw new System.NotImplementedException ();
}
public void FreeDisk(GameObject disk){
diskFactory.FreeDisk(disk);
}
#region IUserAction implementation
public bool Check(){//检查比赛是否结束
return roundController.isEnd;
}
public void Hit(Vector3 position){//光标拾取多个物体
Camera ca = Camera.main;;
Ray ray = ca.ScreenPointToRay(position);
//将飞碟移至底端,触发动作回调
//移动位置
//积分
//更新GUI数据,更新得分
RaycastHit hit;
if (Physics.Raycast(ray, out hit)) {
print (hit.transform.gameObject.name);
if (hit.collider.gameObject.GetComponent<DiskData>() != null) { //plane tag
Debug.Log ("hit " + hit.collider.gameObject.name +"!" );
//将飞碟移至底端,触发动作回调
//移动位置
hit.collider.gameObject.transform.position=new Vector3(0,-7,0);
//积分
roundController.Record(hit.collider.gameObject.GetComponent<DiskData>());
points=roundController.GetPoints();
}
}
}
public int GetPoints(){
return points;
}
public void Restart()//游戏重新开始
{
//userGUI.SetMessage(" ");
//清零得分
//userGUI.SetPoints(0);
roundController.Reset();
points=0;
}
public void SetMode(bool isInfinite){
roundController.SetMode(isInfinite);
}
public void setFlyMode(bool isPhysis){
roundController.setFlyMode(isPhysis);
}
#endregion
void Update(){
}
}
IActionManager
对飞行动作的实现
public interface IActionManager
{
void Fly(GameObject disk,float speed,Vector3 direction);
}
CCFlyAction
运动学的飞行动作
public class CCFlyAction : SSAction
{
public Vector3 direction;
public float speed;
float gravity;
float time;
//public FirstController sceneController;
public static CCFlyAction GetSSAction(Vector3 direction,float speed){
CCFlyAction action = ScriptableObject.CreateInstance<CCFlyAction> ();
action.direction=direction;
action.speed=speed;
action.gravity=9.8f;
action.time=0;
return action;
}
public override void Update ()
{
//获取上一帧渲染所消耗的时间
time+=Time.deltaTime;
//使物体沿着重力方向下落
transform.Translate(Vector3.down*gravity*time*Time.deltaTime);
//在每一帧更新物体的位置,使运动与帧率无关
transform.Translate(direction*speed*Time.deltaTime);
// this.transform.position = Vector3.MoveTowards (this.transform.position, target, speed * Time.deltaTime);
if (this.transform.position.y < -10) {//如果飞碟到达底部,则动作结束,回调
//waiting for destroy
this.enable=false;
this.destory = true;
this.callback.SSActionEvent (this);
}
}
public override void Start () {
//将对象设为刚体
gameobject.GetComponent<Rigidbody>().isKinematic=true;
}
}
CCActionManager
对运动学飞行动作的实现和控制
public class CCActionManager : SSActionManager, ISSActionCallback, IActionManager{
private FirstController sceneController;
public CCFlyAction flyAction;
protected new void Start() {
sceneController = (FirstController)SSDirector.getInstance ().currentSceneController;
sceneController.actionManager=this;
}
// Update is called once per frame
public void Fly(GameObject disk,float speed,Vector3 direction){//实现飞行动作
flyAction=CCFlyAction.GetSSAction(direction,speed);
RunAction(disk,flyAction,this);
}
#region ISSActionCallback implementation
public void SSActionEvent (SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null)
{
//飞碟结束飞行后回收飞碟
sceneController.FreeDisk(source.gameobject);
}
#endregion
}
PhysisFlyAction
对物理学飞行动作的实现
public class PhysisFlyAction : SSAction
{//实现物体的物理学飞行,增加一个水平初速度即可
float speed;//水平速度
Vector3 direction;//飞行方向
public static PhysisFlyAction GetSSAction(Vector3 direction,float speed){
PhysisFlyAction action = ScriptableObject.CreateInstance<PhysisFlyAction> ();
action.direction=direction;
action.speed=speed;
return action;
}
// Start is called before the first frame update
public override void Start()
{
//增加一个水平初速度
gameobject.GetComponent<Rigidbody>().velocity=speed*direction;
}
// Update is called once per frame
public override void Update()
{
if (this.transform.position.y < -10) {//如果飞碟到达底部,则动作结束,回调
//waiting for destroy
this.enable=false;
this.destory = true;
this.callback.SSActionEvent (this);
}
}
}
PhysisActionManager
对物理学飞行动作的控制
public class PhysisActionManager : SSActionManager, ISSActionCallback, IActionManager{
private FirstController sceneController;
public PhysisFlyAction flyAction;
protected new void Start() {
sceneController = (FirstController)SSDirector.getInstance ().currentSceneController;
sceneController.actionManager=this;
}
// Update is called once per frame
public void Fly(GameObject disk,float speed,Vector3 direction){//实现飞行动作
disk.GetComponent<Rigidbody>().isKinematic = false;
disk.GetComponent<Rigidbody>().useGravity = true;
flyAction=PhysisFlyAction.GetSSAction(direction,speed);
RunAction(disk,flyAction,this);
}
#region ISSActionCallback implementation
public void SSActionEvent (SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null)
{
//飞碟结束飞行后回收飞碟
sceneController.FreeDisk(source.gameobject);
}
#endregion
}
ScoreRecorder
对得分的控制
public class ScoreRecorder
{
static int points=0;//记录游戏当前比分
public ScoreRecorder(){
points=0;
}
public void Record(DiskData disk){
//Debug.Log("1\n");
points +=disk.points;
}
public int GetPoints(){
return points;
}
public void Reset(){
points=0;
}
}
RoundController
对每回合的释放的飞碟数的控制
public class RoundController : MonoBehaviour
{
IActionManager actionManager;
DiskFactory diskFactory;
ScoreRecorder scoreRecorder;
int[] roundDisks;//对应回合的飞碟数
bool isInfinite;//游戏当前模式
int round;//游戏当前轮次
int sendCnt;//当前已发送飞碟
float sendTime;//发送时间
public bool isEnd;//游戏是否结束
// Start is called before the first frame update
void Start()
{
actionManager=Singleton<CCActionManager>.Instance;
diskFactory=Singleton<DiskFactory>.Instance;
Debug.Log("diskFactory...\n");
scoreRecorder=new ScoreRecorder();
sendCnt=0;
round=0;
sendTime=0;
isInfinite=false;
isEnd=false;
roundDisks=new int[]{3,5,8,13,21};
}
public void Reset()
{
sendCnt=0;
round=0;
sendTime=0;
isEnd=false;
scoreRecorder.Reset();
//userGUI.SetMessage("");
}
public void Record(DiskData disk){
scoreRecorder.Record(disk);
}
public int GetPoints(){
return scoreRecorder.GetPoints();
}
public void SetMode(bool isInfinite){
this.isInfinite=isInfinite;
}
public void setFlyMode(bool isPhysis){
actionManager=isPhysis? Singleton<PhysisActionManager>.Instance as IActionManager: Singleton<CCActionManager>.Instance as IActionManager;
}
public void SendDisk(){//发送飞碟
//从工厂生成一个飞碟
GameObject disk=diskFactory.GetDisk();
Debug.Log("getdisk...\n");
//设置随机位置
disk.transform.position=new Vector3(-disk.GetComponent<DiskData>().direction.x*7,UnityEngine.Random.Range(3f,8f),0);
disk.SetActive(true);
actionManager.Fly(disk,disk.GetComponent<DiskData>().speed,disk.GetComponent<DiskData>().direction);
}
// Update is called once per frame
void Update()
{
sendTime+=Time.deltaTime;
//每隔1s发送一次飞碟
if(sendTime>1)
{
sendTime=0;
//每次最多发送3个飞碟
for(int i=0;i<2&&sendCnt<roundDisks[round];i++)
{
sendCnt++;
this.SendDisk();
}
}
//判断是否需要重置轮次
if(sendCnt>=roundDisks[round]&&round==(roundDisks.Length-1))
{//最后一轮结束
if(isInfinite){
round=0;
sendCnt=0;
isEnd=false;
}
else{
isEnd=true;
}
}
//更新轮次
if(sendCnt>=roundDisks[round]&&round<(roundDisks.Length-1))
{
sendCnt=0;
round++;
}
}
}
UserGUI
用户界面与游戏的对接,包括游戏界面的呈现
public class UserGUI : MonoBehaviour
{
IUserAction action;
public string gameMessage ;
int points;
void Start()
{
Debug.Log("UserGUI...\n");
points=0;
gameMessage=" ";
action=SSDirector.getInstance().currentSceneController as IUserAction;
}
public void SetMessage(string gameMessage)
{
this.gameMessage=gameMessage;
}
public void SetPoints(int points)
{
this.points=points;
}
private void OnGUI()
{
//小字体
GUIStyle fontStyle = new GUIStyle();
fontStyle.fontSize = 30;
fontStyle.normal.textColor = Color.white;
//大字体
GUIStyle bigStyle = new GUIStyle();
bigStyle.fontSize = 50;
bigStyle.normal.textColor = Color.white;
if(action.Check()){
SetMessage("GameOver!");
}
else{
SetMessage("");
}
SetPoints(action.GetPoints());
if (GUI.Button(new Rect(20, 50, 100, 40), "Restart"))
{
SetMessage("");
//清零得分
SetPoints(0);
action.Restart();
}
if (GUI.Button(new Rect(20, 100, 100, 40), "Normal Mode"))
{
action.SetMode(false);
}
if (GUI.Button(new Rect(20, 150, 100, 40), "Infinite Mode"))
{
action.SetMode(true);
}
if (GUI.Button(new Rect(20, 200, 100, 40), "Kinematics"))
{
action.setFlyMode(false);
}
if (GUI.Button(new Rect(20, 250, 100, 40), "Physis"))
{
action.setFlyMode(true);
}
//返回鼠标点击的位置
if(Input.GetButtonDown("Fire1"))//获取鼠标点击
{
Debug.Log ("Fired Pressed");
action.Hit(Input.mousePosition);
}
GUI.Label(new Rect(300,30,50,200),"Hit UFO",bigStyle);
GUI.Label(new Rect(20,0,100,50),"Points: "+points,fontStyle);
Debug.Log("points:"+points+"\n");
Debug.Log("message:"+gameMessage+"\n");
GUI.Label(new Rect(310, 100, 50, 200), gameMessage, fontStyle);
}
}
游戏完整代码位置:Hit UFO · 张菁/3D游戏编程与设计 - 码云 - 开源中国 (gitee.com)
游戏页面如下:
游戏开始时:
游戏结束时: