游戏设计
- 游戏规则:
游戏有 n 个 round,每个 round 都包括10 次 trial。
每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。
每个 trial 的飞碟有随机性,总体难度随 round 上升。
鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
- 玩家动作表(游戏规则表):
动作 | 条件 | 结果 |
鼠标点击红色飞碟(大小为1) | 游戏未结束(round<5) | 分数加3 |
鼠标点击绿色飞碟(大小为2) | 游戏未结束 | 分数加2 |
鼠标点击蓝色飞碟(大小为3) | 游戏未结束 | 分数加1 |
该round飞碟全部销毁 | 游戏未结束 | round加1 |
- 类图
项目资源
项目演示
代码实现
动作板块
- SSAction 动作基类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSAction : ScriptableObject
{
public bool enable = true;
public bool destroy = false;
public GameObject gameObject { get; set;}
public Transform transform {get; set;}
public IActionCallback callback {get; set;}
protected SSAction() {}
// Start is called before the first frame update
public virtual void Start()
{
throw new System.NotImplementedException();
}
// Update is called once per frame
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
- CCFlyAction 飞碟动作类
飞碟的运动需要两个属性——水平方向速度和竖直方向速度。
飞碟创建时出现在摄像机视角边缘,并逐渐飞向另一个边缘。在它被玩家点击或者飞出摄像机范围内时,飞碟需要被销毁。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCFlyAction : SSAction
{
public float speedX;
public float speedY;
public static CCFlyAction GetSSAction(float x, float y) {
CCFlyAction action = ScriptableObject.CreateInstance<CCFlyAction>();
action.speedX = x;
action.speedY = y;
return action;
}
// Start is called before the first frame update
public override void Start()
{
}
public override void Update()
{
if (this.transform.gameObject.activeSelf == false) {
Debug.Log("1");
this.destroy = true;
this.callback.SSActionEvent(this);
return;
}
Vector3 vec3 = Camera.main.WorldToScreenPoint (this.transform.position);
if (vec3.x < -100 || vec3.x > Camera.main.pixelWidth + 100 || vec3.y < -100 || vec3.y > Camera.main.pixelHeight + 100) {
Debug.Log("2");
this.destroy = true;
this.callback.SSActionEvent(this);
return;
}
transform.position += new Vector3(speedX, speedY, 0) * Time.deltaTime * 2;
}
}
- IActionCallback 事件回调接口
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum SSActionEventType:int {Started, Completed}
public interface IActionCallback
{
//回调函数
void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Completed,
int intParam = 0,
string strParam = null,
Object objectParam = null);
}
- SSActionManager 动作管理类基类
使用对象池对飞碟的生成和销毁进行管理,并且判断每回合中剩余的动作数量(即飞碟数量),在飞碟数量为0时进入下一回合。
#对象池伪代码
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
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>();
// Start is called before the first frame update
protected void Start()
{
}
// 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.destroy) {
waitingDelete.Add(ac.GetInstanceID());
}
else if (ac.enable) {
//Debug.Log("ssactionmanager update");
ac.Update();
}
}
foreach(int key in waitingDelete) {
SSAction ac = actions[key];
actions.Remove(key);
Destroy(ac);
}
waitingDelete.Clear();
}
public void RunAction(GameObject gameObject, SSAction action, IActionCallback manager) {
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.callback = manager;
waitingAdd.Add(action);
action.Start();
}
public int RemainActionCount() {
return actions.Count;
}
}
- CCActionManager 动作管理类
实现飞碟对象的回调事件后的销毁事件。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCActionManager : SSActionManager, IActionCallback
{
public RoundController sceneController;
public CCFlyAction action;
public DiskFactory factory;
// Start is called before the first frame update
protected new void Start()
{
sceneController = (RoundController)SSDirector.getInstance().currentSceneController;
sceneController.actionManager = this;
factory = Singleton<DiskFactory>.Instance;
}
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Completed,
int intParam = 0,
string strParam = null,
Object objectParam = null) {
factory.FreeDisk(source.transform.gameObject);
}
public void MoveDisk(GameObject disk) {
action = CCFlyAction.GetSSAction(disk.GetComponent<DiskAttributes>().speedX, disk.GetComponent<DiskAttributes>().speedY);
RunAction(disk, action, this);
}
}
控制板块
- SSDirector 导演类(代码略)
- 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;
}
}
}
- ISceneController 场景控制器接口
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneController
{
void LoadSource();
void GetHit();
}
- DiskFactory 飞碟工厂类
飞碟工厂用于生产飞碟。飞碟具有颜色、大小、速度等属性(有随机初始化的分数来决定),在摄像机视野边缘初始化位置随机。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyException : System.Exception
{
public MyException() { }
public MyException(string message) : base(message) { }
}
public class DiskAttributes : MonoBehaviour
{
//public GameObject gameobj;
public int score;
public float speedX;
public float speedY;
}
public class DiskFactory : MonoBehaviour
{
List<GameObject> used;
List<GameObject> free;
System.Random rand;
// Start is called before the first frame update
void Start()
{
used = new List<GameObject>();
free = new List<GameObject>();
rand = new System.Random();
//Disk disk = GetDisk(1);
}
// Update is called once per frame
void Update()
{
}
public GameObject GetDisk(int round) {
GameObject disk;
if (free.Count != 0) {
disk = free[0];
//used.Add(free[0]);
free.Remove(disk);
//disk.SetActive(true);
}
else {
disk = GameObject.Instantiate(Resources.Load("Prefabs/disk", typeof(GameObject))) as GameObject;
disk.AddComponent<DiskAttributes>();
//used.Add(disk.GetComponent<DiskAttributes>());
}
disk.transform.localEulerAngles = new Vector3(-rand.Next(20,40),0,0);
DiskAttributes attri = disk.GetComponent<DiskAttributes>();
attri.score = rand.Next(1,4);
//由分数来决定速度、颜色、大小
attri.speedX = (rand.Next(1,5) + attri.score + round) * 0.2f;
attri.speedY = (rand.Next(1,5) + attri.score + round) * 0.2f;
if (attri.score == 3) {
disk.GetComponent<Renderer>().material.color = Color.red;
disk.transform.localScale += new Vector3(-0.5f,0,-0.5f);
}
else if (attri.score == 2) {
disk.GetComponent<Renderer>().material.color = Color.green;
disk.transform.localScale += new Vector3(-0.2f,0,-0.2f);
}
else if (attri.score == 1) {
disk.GetComponent<Renderer>().material.color = Color.blue;
}
int direction = rand.Next(1,5);
//print(attri.score);
//print(direction);
//direction = 3;
if (direction == 1) {
disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(0, Camera.main.pixelHeight * 1.5f, 8)));
attri.speedY *= -1;
}
else if (direction == 2) {
disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(0, Camera.main.pixelHeight * 0f, 8)));
}
else if (direction == 3) {
disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(Camera.main.pixelWidth, Camera.main.pixelHeight * 1.5f, 8)));
attri.speedX *= -1;
attri.speedY *= -1;
}
else if (direction == 4) {
disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(Camera.main.pixelWidth, Camera.main.pixelHeight * 0f, 8)));
attri.speedX *= -1;
}
used.Add(disk);
disk.SetActive(true);
Debug.Log("generate disk");
return disk;
}
public void FreeDisk(GameObject disk) {
disk.SetActive(false);
disk.transform.position = new Vector3(0, 0,0);
disk.transform.localScale = new Vector3(2f,0.1f,2f);
if (!used.Contains(disk)) {
throw new MyException("Try to remove a item from a list which doesn't contain it.");
}
Debug.Log("free disk");
used.Remove(disk);
free.Add(disk);
}
}
- RoundController 回合控制器
在每个回合中从工厂获取飞碟,为飞碟绑定动作,令其开始运动,检测回合是否结束和游戏是否结束等变化。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
public class RoundController : MonoBehaviour, ISceneController, IUserAction
{
int round = 0;
int max_round = 5;
float timer = 0.5f;
GameObject disk;
DiskFactory factory ;
public CCActionManager actionManager;
public ScoreController scoreController;
public UserGUI userGUI;
// Start is called before the first frame update
void Start()
{
}
void Update()
{
if (userGUI.mode == 0) return;
GetHit();
gameOver();
if (round > max_round) {
return;
}
timer -= Time.deltaTime;
if (timer <= 0 && actionManager.RemainActionCount() == 0) {
for (int i = 0; i < 10; ++i) {
disk = factory.GetDisk(round);
actionManager.MoveDisk(disk);
//Thread.Sleep(100);
}
round += 1;
if (round <= max_round) {
userGUI.round = round;
}
timer = 4.0f;
}
}
void Awake() {
SSDirector director = SSDirector.getInstance();
director.currentSceneController = this;
director.currentSceneController.LoadSource();
gameObject.AddComponent<UserGUI>();
gameObject.AddComponent<CCActionManager>();
gameObject.AddComponent<ScoreController>();
gameObject.AddComponent<DiskFactory>();
factory = Singleton<DiskFactory>.Instance;
userGUI = gameObject.GetComponent<UserGUI>();
}
public void LoadSource()
{
}
public void gameOver()
{
if (round > max_round && actionManager.RemainActionCount() == 0)
userGUI.gameMessage = "Game Over!";
}
public void GetHit() {
if (Input.GetButtonDown("Fire1")) {
Camera ca = Camera.main;
Ray ray = ca.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit)) {
scoreController.Record(hit.transform.gameObject);
hit.transform.gameObject.SetActive(false);
}
}
}
}
- ScoreController 分数控制器
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreController : MonoBehaviour
{
int score;
public RoundController roundController;
public UserGUI userGUI;
// Start is called before the first frame update
void Start()
{
roundController = (RoundController)SSDirector.getInstance().currentSceneController;
roundController.scoreController = this;
userGUI = this.gameObject.GetComponent<UserGUI>();
}
public void Record(GameObject disk) {
score += disk.GetComponent<DiskAttributes>().score;
userGUI.score = score;
}
}
用户交互板块
- IUserAction 用户动作接口 在roundController中实现其函数
public interface IUserAction {
void gameOver();
void GetHit();
}
- UserGUI 用户界面类(代码不与游戏核心内容相关,略)