游戏设计
游戏简介
一个从左右发射飞碟让人点击的游戏。
规则
规定回合数为3
规定每回合发射次数为5
每次发射的飞碟数为2或3
飞碟有两种颜色,两种大小,其中大飞碟初始1分,小飞碟初始2分;蓝色加2分,红色加1分;除此之外,飞碟的速度大小分为三级,分别加分1、2、3分。
飞碟会从左往右发射,或者从右往左发射,一旦落入云下就会消失(回收)。
游戏启动准备:
脚本UserGUI挂到Main Camera上,FirstSceneController挂到一个空对象上。此外,Main Camera上可以加天空盒组件(Skybox)修饰一下背景。
玩法:
运行后,玩家可以点击左上角的start按钮开始游戏,也可以用restart按钮刷新游戏为准备状态。游戏中可以点击飞碟得分。
游戏演示
打飞碟 - 游戏演示
上面视频中演示了两次游戏。(可能是录屏的原因,鼠标位置似乎偏上了,实际我鼠标是对准船了的)
游戏组成
预制体
代码文件
工厂对象设计
总UML图
飞碟工厂部分UML图
飞碟工厂对象实现为单例类,由回合控制器RoundController使用,工厂对象负责创建飞碟和回收飞碟,并根据回合控制器的要求改造飞碟属性,这样,只有当游戏同一时间需要的飞碟数量大于已有飞碟对象时,才需要创建新的飞碟对象。
游戏代码
场景控制器FirstController
加载一些场景预制体,实现用户动作接口的点击(射线)等方法和场景控制器接口方法。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Xml.Serialization;
using UnityEngine;
/* 游戏状态,0为准备进行,1为正在进行游戏,2为结束 */
enum GameState
{
Ready = 0, Playing = 1, GameOver = 2
};
public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
private RoundController roundController; // 回合控制器
private int gameState;
private void Awake()
{
SSDirector director = SSDirector.getInstance();
director.currentSceneController = this;
director.currentSceneController.LoadResources();
}
void Start()
{
roundController = gameObject.AddComponent<RoundController>();
gameState = (int)GameState.Ready;
}
public void LoadResources()
{
// 云
GameObject plane = GameObject.Instantiate(Resources.Load<GameObject>("Prefabs/Plane"), Vector3.zero, Quaternion.identity);
}
public void SetGameState(int state)
{
gameState = state;
}
public int GetGameState()
{
return gameState;
}
public void Restart()
{
gameState = (int)GameState.Ready;
roundController.Reset();
}
public void start()
{
if(gameState == (int)GameState.Ready)
gameState = (int)GameState.Playing;
}
public string check()
{
if(this.gameState == (int)GameState.GameOver)
{
return "GameOver!\nYour Score:"+roundController.GetScores();
}
return null;
}
public void shoot()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
if (hit.collider.gameObject.GetComponent<DiskData>() != null)
{
// 把击中的飞碟移出屏幕,触发回调释放
hit.collider.gameObject.transform.position = new Vector3(0, -6, 0);
// 记录飞碟得分
roundController.Record(hit.collider.gameObject.GetComponent<DiskData>());
}
}
}
}
}
实现的接口:
using System;
public interface IUserAction
{
void Restart();
void start();
void shoot();
string check();
}
using System;
public interface ISceneController
{
void LoadResources();
}
用户GUI界面UserGUI
调用IUserAction方法,实现点击功能、游戏结束提示
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour {
private IUserAction action;
private GUIStyle style;
void Start () {
action = SSDirector.getInstance ().currentSceneController as IUserAction;
}
void OnGUI()
{
float width = Screen.width / 8;
float height = Screen.height / 12;
if (GUI.Button(new Rect(0, 0, width, height), "Start"))
{
action.start();
}
if (GUI.Button(new Rect(width, 0, width, height), "ReStart"))
{
action.Restart();
}
if (action.check() != null )
{
GUI.Box(new Rect(width * 3.5f, height * 5, width*2, height*2), action.check());
}
}
void Update()
{
if (((FirstController)SSDirector.getInstance().currentSceneController).GetGameState() == 1)
action.shoot();
}
}
导演类SSDirector
管理场景
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SSDirector : System.Object
{
private static SSDirector instance;
public ISceneController currentSceneController { get; set; }
public static SSDirector getInstance()
{
if (instance == null)
{
instance = new SSDirector();
}
return instance;
}
}
单例模板类
用于工厂类的单实例化
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;
}
}
}
飞碟数据
飞碟大小、速度大小和颜色。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskData : MonoBehaviour
{
// Start is called before the first frame update
public int size;
public int speed;
public Color color;
}
飞碟工厂对象
工厂对象中实现一个对象池,将飞碟对象分在被使用和没有使用两个队列,可以在飞碟使用完后回收,减少飞碟对象的重复创建。
using System.Collections.Generic;
using System.Data;
using UnityEngine;
public class DiskFactory : MonoBehaviour
{
public GameObject diskPrefab; // 飞碟游戏对象,创建新的飞碟游戏对象的复制对象
private List<DiskData> used; // 正在被游戏使用的飞碟对象
private List<DiskData> free; // 没有被使用的空闲飞碟对象
public void Start()
{
diskPrefab = GameObject.Instantiate(Resources.Load<GameObject>("Prefabs/Disk"), Vector3.zero, Quaternion.identity);
diskPrefab.SetActive(false);
used = new List<DiskData>();
free = new List<DiskData>();
}
// 飞碟获取方法,根据ruler获取相应飞碟
public GameObject GetDisk(Ruler ruler)
{
GameObject disk;
// 从缓存中获取飞碟,没有则先创建
int diskNum = free.Count;
if (diskNum == 0)
{
disk = GameObject.Instantiate(diskPrefab, Vector3.zero, Quaternion.identity);
disk.AddComponent(typeof(DiskData));
}
else
{
disk = free[diskNum - 1].gameObject;
free.Remove(free[diskNum - 1]);
}
// 根据ruler设置disk的速度、颜色、大小、飞入方向
disk.GetComponent<DiskData>().speed = ruler.speed;
disk.GetComponent<DiskData>().color = ruler.color;
disk.GetComponent<DiskData>().size = ruler.size;
// 给飞碟上颜色
disk.GetComponent<Renderer>().material.color = ruler.color;
// 绘制飞碟大小
disk.transform.localScale = new Vector3(1.5f * (float)ruler.size, 0.2f, 1.5f * (float)ruler.size);
// 绘制碰撞器大小
disk.GetComponent<BoxCollider>().size = disk.transform.localScale;
// 选择飞碟飞入屏幕的起始位置
disk.transform.position = ruler.beginPos;
// 设置飞碟显示
disk.SetActive(true);
// 将飞碟加入使用队列
used.Add(disk.GetComponent<DiskData>());
return disk;
}
// 飞碟回收方法,将不使用的飞碟从使用队列放到空闲队列中
public void FreeDisk(GameObject disk)
{
foreach (DiskData d in used)
{
if (d.gameObject.GetInstanceID() == disk.GetInstanceID())
{
disk.SetActive(false);
used.Remove(d);
free.Add(d);
break;
}
}
}
}
动作管理器部分
动作类(抽象类)SSAction
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSAction : ScriptableObject {
public bool enable = true;
public bool destory = false;
public GameObject gameobject { get; set; }
public Transform transform { get; set; }
public ISSActionCallback callback { get; set; }
protected SSAction () {}
// Use this for initialization
public virtual void Start () {
throw new System.NotImplementedException ();
}
// Update is called once per frame
public virtual void Update () {
throw new System.NotImplementedException ();
}
}
飞碟具体动作类FlyAction
GetSSAction方法在动作管理器CCActionManager中调用,获取动作信息(目标向量和速度)。FlyAction设置对应飞碟对象的初速度,在Update中进行动作结束判断,并由回调函数回收飞碟。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FlyAction : SSAction
{
float speed; // 速度大小
Vector3 direction; // 飞行方向
public static FlyAction GetSSAction(Vector3 direction, float speed)
{
FlyAction action = ScriptableObject.CreateInstance<FlyAction>();
action.speed = speed;
action.direction = direction;
return action;
}
public override void Start()
{
gameobject.GetComponent<Rigidbody>().isKinematic = false;
gameobject.GetComponent<Rigidbody>().velocity = speed * direction;
}
public override void Update()
{
// 飞碟到达底部动作结束,回调
if (this.transform.position.y < -1)
{
this.destory = true;
this.enable = false;
this.callback.SSActionEvent(this);
}
}
}
动作控制器基类SSActionManager
RunAction方法将待执行动作加入执行队列中,在Update中执行,执行过的动作加入销毁队列销毁。不过游戏要求同一时间只有一个动作,所以用了裁判类判断来限制了动作的生成和积累。
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> ();
// Update is called once per frame
protected void Update () {
foreach (SSAction ac in waitingAdd) actions [ac.GetInstanceID ()] = ac;
waitingAdd.Clear ();
foreach (KeyValuePair <int, SSAction> kv in actions) {
SSAction ac = kv.Value;
if (ac.destory) {
waitingDelete.Add(ac.GetInstanceID()); // release action
} else if (ac.enable) {
ac.Update (); // update action
}
}
foreach (int key in waitingDelete) {
SSAction ac = actions[key];
actions.Remove(key);
Object.Destroy(ac);
}
waitingDelete.Clear ();
}
public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager) {
action.gameobject = gameobject;
action.transform = gameobject.transform;
action.callback = manager;
waitingAdd.Add (action);
action.Start ();
}
// Use this for initialization
protected void Start () {
}
}
动作控制器类CCActionManager
实现动作管理器接口和回调函数。
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class PhysisActionManager : SSActionManager, ISSActionCallback, IActionManager
{
FlyAction PlayDiskAction; // 飞碟空中动作
public void PlayDisk(GameObject disk, float speed, Vector3 direction)
{
PlayDiskAction = FlyAction.GetSSAction(direction, speed);
RunAction(disk, PlayDiskAction, this);
}
// 回调函数
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competed,
int intParam = 0,
string strParam = null,
Object objectParam = null)
{
// 结束飞行后回收飞碟
Singleton<RoundController>.Instance.FreeFactoryDisk(source.gameobject);
}
}
实现的接口:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum SSActionEventType:int { Started, Competeted }
public interface ISSActionCallback
{
void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0 ,
string strParam = null,
Object objectParam = null);
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface IActionManager
{
void PlayDisk(GameObject disk, float speed, Vector3 direction);
}
规则Ruler类
游戏中的回合数,每个回合的发射数,以及回合控制器调用飞碟工厂获取对应飞碟、设置动作时需要的飞碟数据。
using System.Collections.Generic;
using System.Data;
using UnityEngine;
// 飞碟获取规则
public struct Ruler
{
public int trialNum; // 当前trial的编号
public int roundNum; // 当前round的编号
public int roundSum; // 一共round的总数目
public int[] roundDisksNum; // 每一轮对于trial的飞碟数量
public float sendTime; // 发射间隔时间
public int size; // 飞碟大小
public int speed; // 飞碟速度
public Color color; // 飞碟颜色
public Vector3 direction; // 飞碟飞入方向
public Vector3 beginPos; // 飞碟飞入位置
};
回合控制器类
实现获取飞碟、设置动作等具体过程。
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class RoundController : MonoBehaviour
{
private IActionManager actionManager; // 选择飞碟的运动类型
private ScoreRecorder scoreRecorder; // 记分器
private FirstController mainController;
private Ruler ruler; // 飞碟获取规则
void Start()
{
actionManager =gameObject.AddComponent<PhysisActionManager>();
scoreRecorder = new ScoreRecorder();
mainController = (FirstController)SSDirector.getInstance().currentSceneController;
gameObject.AddComponent<DiskFactory>();
InitRuler();
}
void InitRuler()
{
ruler.trialNum = 0;
ruler.roundNum = 0;
ruler.sendTime = 0;
ruler.roundSum = 3;
ruler.roundDisksNum = new int[10];
generateRoundDisksNum();
}
// 生成每trial同时发出的飞碟数量的数组,同时发出飞碟个数不超过3
public void generateRoundDisksNum()
{
for (int i = 0; i < 5; ++i)
{
ruler.roundDisksNum[i] = Random.Range(2, 4);
}
}
public void Reset()
{
InitRuler();
scoreRecorder.Reset();
FreeAllFactoryDisk();
}
public void Record(DiskData disk)
{
scoreRecorder.Record(disk);
}
public int GetScores()
{
return scoreRecorder.score;
}
// 发射飞碟
public void LaunchDisk()
{
// 使飞碟飞入位置尽可能分开,从不同位置飞入使用的数组
for (int i = 0; i < ruler.roundDisksNum[ruler.trialNum]; ++i)
{
// 获取随机数
int randomNum = Random.Range(0, 3) + 1;
// 飞碟速度随回合数增加而变快,这样难度增加
ruler.speed = randomNum;
// 重新选取随机数,并根据随机数选择飞碟颜色
randomNum = Random.Range(0, 2) + 1;
if (randomNum == 1)
{
ruler.color = Color.red;
}
else
{
ruler.color = Color.blue;
}
// 重新选取随机数,并根据随机数选择飞碟的大小
ruler.size = Random.Range(0, 2) + 1;
// 从左到右 或者 从右到左
if(Random.Range(0, 2) == 0)
{
ruler.direction = new Vector3(5, 5, 0);
// 重新选取随机数,并使不同飞碟的飞入位置尽可能分开
ruler.beginPos = new Vector3(-15, (Random.Range(0, 3) + 1) * 2, 0);
}
else
{
ruler.direction = new Vector3(-5, 5, 0);
// 重新选取随机数,并使不同飞碟的飞入位置尽可能分开
ruler.beginPos = new Vector3(15, (Random.Range(0, 3)+1)* 2, 0);
}
// 根据ruler从工厂中生成一个飞碟
GameObject disk = Singleton<DiskFactory>.Instance.GetDisk(ruler);
// 设置飞碟的飞行动作
actionManager.PlayDisk(disk, ruler.speed, ruler.direction);
}
}
// 释放工厂飞碟
public void FreeFactoryDisk(GameObject disk)
{
Singleton<DiskFactory>.Instance.FreeDisk(disk);
}
// 释放所有工厂飞碟
public void FreeAllFactoryDisk()
{
GameObject[] obj = FindObjectsOfType(typeof(GameObject)) as GameObject[];
foreach (GameObject g in obj)
{
if (g.gameObject.name == "Disk(Clone)(Clone)")
{
Singleton<DiskFactory>.Instance.FreeDisk(g);
}
}
}
void Update()
{
Debug.Log(mainController.GetGameState());
if (mainController.GetGameState() == (int)GameState.Playing)
{
Debug.Log("roundSum:" + ruler.roundSum);
ruler.sendTime += Time.deltaTime;
// 每隔2s发送一次飞碟(trial)
if (ruler.sendTime > 2)
{
Debug.Log("roundNum:" + ruler.roundNum);
ruler.sendTime = 0;
// 如果为无限回合或还未到设定回合数
if ( ruler.roundNum < ruler.roundSum)
{
Debug.Log("trialNum:"+ ruler.trialNum);
// 发射飞碟,次数trial增加
LaunchDisk();
ruler.trialNum++;
// 当次数trial等于2时,说明一个回合已经结束,回合加一,重新生成飞碟数组
if (ruler.trialNum == 5)
{
ruler.trialNum = 0;
ruler.roundNum++;
generateRoundDisksNum();
}
}
// 否则游戏结束,提示重新进行游戏
else
{
mainController.SetGameState((int)GameState.GameOver);
}
}
}
}
}
计分器
完成每次游戏的计分任务,由回合控制器控制。
using System.Collections;
using System.Collections.Generic;
using System.Data;
using UnityEngine;
public class ScoreRecorder
{
public int score; // 游戏分数
public ScoreRecorder()
{
score = 0;
}
/* 记录分数,根据点击中的飞碟的大小,速度,颜色计算得分 */
public void Record(DiskData disk)
{
// 飞碟越小分就越高,大小为1(1.5×1.5×1)得2分,大小(2×2×1.5)为2得1分
int diskSize = disk.size;
switch (diskSize)
{
case 1:
score = 2;
break;
case 2:
score = 1;
break;
default: break;
}
// 颜色为红色得1分,颜色为蓝色得2分
Color diskColor = disk.color;
if (diskColor == Color.red)
{
score += 1;
}
else if (diskColor == Color.blue)
{
score += 2;
}
// 速度越快分就越高
score += disk.speed;
}
/* 重置分数,设为0 */
public void Reset()
{
score = 0;
}
}