1. 游戏要求
- 分多个 round , 每个 round 都是 n 个 trail
- 每个 trail 的飞碟的色彩,大小;发射位置,速度,角度,每次发射飞碟数量不一
- 鼠标击中得分,得分按色彩、大小、速度不同计算,计分规则自由定
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
- 近可能使用前面 MVC 结构实现人机交互与游戏模型分离
- 必须使用对象池管理飞碟对象
- 建议使用 ScriptableObject 配置不同的飞碟
- 建议使用物理引擎管理飞碟飞行路径
2.游戏规则
游戏每round有10个disk抛出,round完成后玩家可以选择进入下一round。round=3时,游戏结束,得分>60即为游戏胜利。
积分规则:disk有三种颜色,白色、灰色、黑色,对应的分数为1,2,4。
3.前置知识
工厂模式Factory Pattern
工厂模式提供了一种将对象的实例化过程封装在工厂类中的方式。通过使用工厂模式,可以将对象的创建与使用代码分离,提供一种统一的接口来创建不同类型的对象。
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
简单工厂又称为工厂方法,即类一个方法能够得到一个对象实例,使用者不需要知道该实例如何构建、初始化等细节。
- 游戏对象的创建与销毁高成本,必须减少销毁次数。如:游戏中子弹
- 屏蔽创建与销毁的业务逻辑,使程序易于扩展
优点: 1、一个调用者想创建一个对象,只要知道其名称就可以了。 2、扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 3、屏蔽产品的具体实现,调用者只关心产品的接口。
在 Unity 中,工厂方法 + 单实例 + 对象池 通常都是同时一起使用。
单实例
这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 避免对资源的多重占用(比如写文件操作)。
对象池
对象池(Object Pool) - 知乎 (zhihu.com)
当创建对象时,对象池将对象放入池管理的某种内存连续的数据结构中(数组或者栈等)。当不需要对象时,对象池并不销毁对象,而是将对象回收到池中,下次需要的时候再次从池中拿出来。
因为,对象储存在内存连续的数据结构中,所以解决了内存碎片的问题。
因为,对象每次用完以后就放回池中循环利用而不是再次创建和销毁,这样就解决了频繁的内存分配和销毁的问题。
4.游戏设计
场景控制类
- 导演类Director,单例模式,继承System.Object(会不被Unity内存管理,但所有Scene都能访问到它),主要控制场景切换(虽然现在只有一个场景)。
- 接口场景类ISceneController,负责指明具体实现的场景类要实现的方法,而且便于更多的类能通过接口来访问场景类,由FirstSceneController具体场景实现类来实现。
工厂模式
- 飞碟数据类DiskData,说明当前飞碟的状态,用于描述飞碟。
- 模板类Singleton,用于给需要的类生成一个唯一的实例。
- 飞碟工厂类DiskFactory,用于制造和销毁飞碟的工厂。
动作分离和动作控制类
- 接口类IUserAction,负责指明由用户行为引发的变化的方法,由FirstSceneController这个最高级的控制类来实现。
- 所有动作的基础类SSAction,用于规定所有动作的基础规范,继承ScriptableObject(ScriptableObject是不需要绑定GameObject对象的可编程基类,这些基类受Unity引擎场景管理)。
- 飞碟飞行动作类CCFlyAction。
- 组合动作管理类SSActionManager,用于管理一系列的动作,负责创建和销毁它们。
事件和逻辑控制类
- 最高级的控制类FirstSceneController,负责底层数据与用户操作的GUI的交互,实现ISceneControl和IUserAction。
- 动作事件接口类ISSActionCallback,定义了事件处理的接口,事件管理器必须实现它。
- 事件管理类CCActionManager,继承了SSActionManager,实现了ISSActionCallback,负责事件的处理。
- 用户界面类UserGUI,负责生成界面交于用户操作。
- 记分员ScoreRecorder,用于给用户计分。
5.代码
CCAction类
sceneControl
是FirstSceneControl
类型的变量,用于管理场景控制。flys
是CCFlyAction
动作的列表。diskNumber
跟踪当前场景中的飞行盘数量。SSActionEvent
方法处理动作事件,特别是当动作类型为CCFlyAction
时。它减少diskNumber
计数,释放对应的飞行盘对象,并调用freeSSAction
方法释放动作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCActionManager : SSActionManager, ISSActionCallback {
private FirstSceneControl sceneControl;
private List<CCFlyAction> flys = new List<CCFlyAction>();
private int diskNumber = 0;
private List<SSAction> used = new List<SSAction>();
private List<SSAction> free = new List<SSAction>();
public void setDiskNumber(int dn)
{
diskNumber = dn;
}
public int getDiskNumber()
{
return diskNumber;
}
public SSAction getSSAction()
{
SSAction action = null;
if(free.Count > 0)
{
action = free[0];
free.Remove(free[0]);
}
else
{
action = ScriptableObject.Instantiate<CCFlyAction>(flys[0]);
}
used.Add(action);
return action;
}
public void freeSSAction(SSAction action)
{
foreach(SSAction a in used)
{
if(a.GetInstanceID() == action.GetInstanceID())
{
a.reset();
free.Add(a);
used.Remove(a);
break;
}
}
}
protected void Start()
{
sceneControl = (FirstSceneControl)Director.getInstance().current;
sceneControl.actionManager = this;
flys.Add(CCFlyAction.getCCFlyAction());
}
private new void Update()
{
if (sceneControl.getGameState() == GameState.RUNNING)
base.Update();
}
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Completed, int intPram = 0
, string strParm = null, Object objParm = null)
{
if(source is CCFlyAction)
{
diskNumber--;
Singleton<DiskFactory>.Instance.freeDisk(source.gameObject);
freeSSAction(source);
}
}
public void startThrow(Queue<GameObject> diskQueue)
{
foreach(GameObject i in diskQueue)
{
runAction(i, getSSAction(), (ISSActionCallback)this);
}
}
}
CCFlyAction类
getCCFlyAction
方法是一个静态方法,用于获取CCFlyAction
的实例。它通过调用ScriptableObject.CreateInstance<CCFlyAction>()
来创建一个新的实例,并返回该实例。-
- 如果飞行盘的 Y 坐标小于 -4,表示飞行盘已经超出视野,触发销毁逻辑。设置
destroy
为 true,禁用该动作,并通过callback.SSActionEvent(this)
触发动作完成事件。
- 如果飞行盘的 Y 坐标小于 -4,表示飞行盘已经超出视野,触发销毁逻辑。设置
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCFlyAction : SSAction
{
float acceleration;
float horizontalSpeed;
Vector3 direction;
float time;
public static CCFlyAction getCCFlyAction()
{
CCFlyAction action = ScriptableObject.CreateInstance<CCFlyAction>();
return action;
}
public override void Start()
{
enable = true;
acceleration = 9.8f;
time = 0;
horizontalSpeed = gameObject.GetComponent<DiskData>().getSpeed();
direction = gameObject.GetComponent<DiskData>().getDirection();
}
public override void Update()
{
if (gameObject.activeSelf)
{
time += Time.deltaTime;
transform.Translate(Vector3.down * acceleration * time * Time.deltaTime);
transform.Translate(direction * horizontalSpeed * Time.deltaTime);
if(this.transform.position.y < -4)
{
this.destroy = true;
this.enable = false;
this.callback.SSActionEvent(this);
}
}
}
}
Director类
控制场景的切换
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Director : System.Object {
public ISceneControl current { set; get; }
private static Director _Instance;
public static Director getInstance()
{
return _Instance ?? (_Instance = new Director());
}
}
IUserAction类
public enum GameState { ROUND_START, ROUND_FINISH, RUNNING, PAUSE, START, FUNISH}
public interface IUserAction{
GameState getGameState();
void setGameState(GameState gameState);
int getScore();
void hit(Vector3 pos);
void reset();
}
DiskData类
size
: 用于存储飞行盘的大小(Vector3
类型)。color
: 用于存储飞行盘的颜色。speed
: 用于存储飞行盘的速度。direction
: 用于存储飞行盘的飞行方向。getSize()
: 获取飞行盘的大小。getSpeed()
: 获取飞行盘的速度。getDirection()
: 获取飞行盘的飞行方向。getColor()
: 获取飞行盘的颜色。setDiskData(Vector3 size, Color color, float speed, Vector3 direction)
: 设置飞行盘的属性。通过传递参数来初始化飞行盘的大小、颜色、速度和飞行方向。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskData : MonoBehaviour {
private Vector3 size;
private Color color;
private float speed;
private Vector3 direction;
public DiskData() { }
public Vector3 getSize()
{
return size;
}
public float getSpeed()
{
return speed;
}
public Vector3 getDirection()
{
return direction;
}
public Color getColor()
{
return color;
}
public void setDiskData(Vector3 size, Color color, float speed, Vector3 direction)
{
this.size = size;
this.color = color;
this.speed = speed;
this.direction = direction;
}
}
DiskFactory类
diskPrefab
: 预制飞行盘的游戏对象,用于实例化新的飞行盘。used
: 保存正在使用中的飞行盘的列表。free
: 保存可重用的飞行盘的列表。getDisk
方法用于获取一个新的飞行盘对象。如果有可重用的飞行盘,则从free
列表中获取;否则,通过实例化diskPrefab
来创建一个新的飞行盘对象。根据传入的round
参数,设置飞行盘的大小、颜色、速度、方向等属性。将新创建或重新使用的飞行盘对象加入到used
列表,并设置其名字和缩放。freeDisk
方法用于释放一个飞行盘对象。通过传入的disk
参数查找对应的DiskData
对象,并将其从used
列表移到free
列表中,同时设置其状态为不激活。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskFactory : MonoBehaviour {
public GameObject diskPrefab;
public List<DiskData> used = new List<DiskData>();
public List<DiskData> free = new List<DiskData>();
private void Awake()
{
diskPrefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/disk"), Vector3.zero, Quaternion.identity);
diskPrefab.SetActive(false);
}
public GameObject getDisk(int round)
{
GameObject disk = null;
if(free.Count > 0)
{
disk = free[0].gameObject;
free.Remove(free[0]);
}
else
{
disk = GameObject.Instantiate<GameObject>(diskPrefab, Vector3.zero, Quaternion.identity);
disk.AddComponent<DiskData>();
}
int start;
switch (round)
{
case 0: start = 0; break;
case 1: start = 100; break;
default: start = 200; break;
}
int selectColor = Random.Range(start, round * 499);
round = selectColor / 250;
DiskData diskData = disk.GetComponent<DiskData>();
Renderer renderer = disk.GetComponent<Renderer>();
Renderer childRenderer = disk.transform.GetChild(0).GetComponent<Renderer>();
float ranX = Random.Range(-1, 1) < 0 ? -1.2f : 1.2f;
Vector3 direction = new Vector3(ranX, 1, 0);
switch (round)
{
case 0:
diskData.setDiskData(new Vector3(1.35f, 1.35f, 1.35f), Color.white, 4.0f, direction);
renderer.material.color = Color.white;
childRenderer.material.color = Color.white;
break;
case 1:
diskData.setDiskData(new Vector3(1f, 1f, 1f), Color.gray, 6.0f, direction);
renderer.material.color = Color.gray;
childRenderer.material.color = Color.gray;
break;
case 2:
diskData.setDiskData(new Vector3(0.7f, 0.7f, 0.7f), Color.black, 8.0f, direction);
renderer.material.color = Color.black;
childRenderer.material.color = Color.black;
break;
}
used.Add(diskData);
diskData.name = diskData.GetInstanceID().ToString();
disk.transform.localScale = diskData.getSize();
return disk;
}
public void freeDisk(GameObject disk)
{
DiskData temp = null;
foreach (DiskData i in used)
{
if (disk.GetInstanceID() == i.gameObject.GetInstanceID())
{
temp = i;
}
}
if (temp != null)
{
temp.gameObject.SetActive(false);
free.Add(temp);
used.Remove(temp);
}
}
}
FirstSceneControl类
actionManager
: 用于管理动作的管理器,包括飞行盘的发射和动作状态。scoreRecorder
: 记录游戏得分的类。diskQueue
: 一个队列,用于存储飞行盘对象。diskNumber
: 一个表示每轮生成的飞行盘数量的变量。currentRound
: 表示当前游戏轮数的变量。time
: 用于计时,控制飞行盘的发射间隔。gameState
: 表示当前游戏状态的枚举变量。- 在
Update
方法中,根据不同的游戏状态执行相应的逻辑。 - 如果当前动作管理器中的飞行盘数量为0,且游戏状态为
RUNNING
,则表示一轮飞行盘已经全部飞出,将游戏状态设为ROUND_FINISH
。如果当前轮数为2,表示游戏结束,将游戏状态设为FUNISH
。 - 如果当前动作管理器中的飞行盘数量为0,且游戏状态为
ROUND_START
,表示上一轮飞行盘已经全部飞出,进入下一轮的准备。增加当前轮数,调用nextRound
方法准备下一轮的飞行盘,同时将动作管理器中的飞行盘数量设置为10,将游戏状态设为RUNNING
。 - 如果计时器超过1秒,且游戏状态不是
PAUSE
,则调用throwDisk
方法抛出飞行盘,并将计时器重置为0。否则,继续递增计时器。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstSceneControl : MonoBehaviour, ISceneControl, IUserAction {
public CCActionManager actionManager { set; get; }
public ScoreRecorder scoreRecorder { set; get; }
public Queue<GameObject> diskQueue = new Queue<GameObject>();
private int diskNumber = 0;
private int currentRound = -1;
private float time = 0;
private GameState gameState = GameState.START;
void Awake()
{
Director director = Director.getInstance();
director.current = this;
diskNumber = 10;
this.gameObject.AddComponent<ScoreRecorder>();
this.gameObject.AddComponent<DiskFactory>();
scoreRecorder = Singleton<ScoreRecorder>.Instance;
director.current.loadResources();
}
public void loadResources()
{
}
private void Update()
{
if(actionManager.getDiskNumber() == 0 && gameState == GameState.RUNNING)
{
gameState = GameState.ROUND_FINISH;
if(currentRound == 2)
{
gameState = GameState.FUNISH;
return;
}
}
if(actionManager.getDiskNumber() == 0 && gameState == GameState.ROUND_START)
{
currentRound++;
nextRound();
actionManager.setDiskNumber(10);
gameState = GameState.RUNNING;
}
if(time > 1 && gameState != GameState.PAUSE)
{
throwDisk();
time = 0;
}
else
{
time += Time.deltaTime;
}
}
private void nextRound()
{
DiskFactory diskFactory = Singleton<DiskFactory>.Instance;
for(int i = 0; i < diskNumber; i++)
{
diskQueue.Enqueue(diskFactory.getDisk(currentRound));
}
actionManager.startThrow(diskQueue);
}
void throwDisk()
{
if(diskQueue.Count != 0)
{
GameObject disk = diskQueue.Dequeue();
Vector3 pos = new Vector3(-disk.GetComponent<DiskData>().getDirection().x * 10, Random.Range(0f, 4f), 0);
disk.transform.position = pos;
disk.SetActive(true);
}
}
public int getScore()
{
return scoreRecorder.score;
}
public GameState getGameState()
{
return gameState;
}
public void setGameState(GameState gameState)
{
this.gameState = gameState;
}
public void hit(Vector3 pos)
{
RaycastHit[] hits = Physics.RaycastAll(Camera.main.ScreenPointToRay(pos));
for(int i = 0; i < hits.Length; i++)
{
RaycastHit hit = hits[i];
if(hit.collider.gameObject.GetComponent<DiskData>() != null)
{
scoreRecorder.record(hit.collider.gameObject);
hit.collider.gameObject.transform.position = new Vector3(0, -5, 0);
}
}
}
public void reset(){
this.currentRound=-1;
scoreRecorder.reset();
}
}
Singleton类
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;
}
}
}
UserGUI类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class UserGUI : MonoBehaviour {
private IUserAction action;
bool isFirst = true;
GUIStyle red;
GUIStyle black;
// Use this for initialization
void Start () {
action = Director.getInstance().current as IUserAction;
black = new GUIStyle("button");
black.fontSize = 20;
red = new GUIStyle();
red.fontSize = 30;
red.fontStyle = FontStyle.Bold;
red.normal.textColor = Color.yellow;
red.alignment = TextAnchor.UpperCenter;
}
private void OnGUI()
{
if (action.getGameState() == GameState.FUNISH)
{
GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height / 2 - 150, 200, 100), action.getScore() >= 60 ? "You win!" : "You fail!", red);
if(GUI.Button(new Rect(Screen.width / 2 - 40, Screen.height / 2 - 40, 120, 40), "Restart", black))
{
// SceneManager.LoadScene("DiskAttack");
action.setGameState(GameState.ROUND_START);
action.reset();
}
return;
}
Rect rect = new Rect(Screen.width / 2 - 400, 0, 200, 40);
Rect rect2 = new Rect(Screen.width / 2 + 245, 20, 120, 40);
if (Input.GetButtonDown("Fire1") && action.getGameState() != GameState.PAUSE)
{
Vector3 pos = Input.mousePosition;
action.hit(pos);
}
if (!isFirst)
{
GUI.Label(rect, "Your score: " + action.getScore().ToString(), red);
}
else
{
GUIStyle blackLabel = new GUIStyle();
blackLabel.fontSize = 16;
blackLabel.normal.textColor = Color.black;
}
if (action.getGameState() == GameState.RUNNING && GUI.Button(rect2, "Paused", black))
{
action.setGameState(GameState.PAUSE);
}
else if(action.getGameState() == GameState.PAUSE && GUI.Button(rect2, "Run", black))
{
action.setGameState(GameState.RUNNING);
}
if (isFirst && GUI.Button(new Rect(Screen.width / 2 -45 , 100, 120, 40), "Start", black))
{
isFirst = false;
action.setGameState(GameState.ROUND_START);
}
if(!isFirst && action.getGameState() == GameState.ROUND_FINISH && GUI.Button(rect2, "Next Round", black))
{
action.setGameState(GameState.ROUND_START);
}
}
}
视频地址:打飞碟小游戏_哔哩哔哩_bilibili