一、游戏介绍
- 游戏有 5 个 round,每个 round 都包括10 次 trial;
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。飞碟根据飞碟样式分别记为1-3分,分数越高,平均移动速度越快;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升,体现为增加所有样式飞碟的速度;
- 鼠标点中飞碟即可得分,全部飞碟被击落或越界完成该 round;
- 可以通过界面中下方的按钮来切换飞碟运动模式,分为运动学模式和物理模式,前者不存在碰撞与反弹,后者反之。
- 玩家可以随时选择重新开始游戏,点击右下角按钮即可。
二、总体框架
1.使用工厂方法 + 单实例 + 对象池。
2.在之前开发基础上使用Adapter模式管理飞碟的两种运动逻辑。
3.其余部分遵照之前开发的MVC模式。
三、代码讲解
1.Controller
场景单实例Singleten<T>
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;
}
}
}
飞碟工厂DiskFactory
飞碟工厂负责管理飞碟的释放与回收,维护了对应的两个列表。同时还负责飞碟的生成逻辑,在这里添加了一些随机性因素使得游玩效果更具多样性。在方向上飞碟会从左上、左下、右上以及右下四个方向随机出现,并且旋转随机的角度。根据难度和飞碟分值确定了平均速度,同时增加了速度随机浮动。
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();
}
public GameObject GetDisk(int round)
{
GameObject disk;
if (free.Count != 0)
{
disk = free[0];
free.Remove(disk);
}
else
{
int IScore = rand.Next(1, 4);
if(IScore==1)
disk = GameObject.Instantiate(Resources.Load("Prefabs/disk1", typeof(GameObject))) as GameObject;
else if (IScore==2)
disk = GameObject.Instantiate(Resources.Load("Prefabs/disk2", typeof(GameObject))) as GameObject;
else
disk = GameObject.Instantiate(Resources.Load("Prefabs/disk3", typeof(GameObject))) as GameObject;
disk.AddComponent<DiskAttributes>();
disk.GetComponent<DiskAttributes>().score = IScore;
}
//根据不同round设置diskAttributes的值
//随意的旋转角度
disk.transform.localEulerAngles = new Vector3(-rand.Next(20, 40), 0, 0);
DiskAttributes attri = disk.GetComponent<DiskAttributes>();
//由分数来决定速度
attri.speedX = (rand.Next(1, 5) + attri.score + round) * 0.1f;
attri.speedY = (rand.Next(1, 5) + attri.score + round) * 0.1f;
//飞碟可从四个方向飞入(左上、左下、右上、右下)
int direction = rand.Next(1, 5);
if (direction == 1)
{
disk.transform.position = new Vector3(-15, 5 + Random.Range(-2, 2), 1);
attri.speedY *= -1;
}
else if (direction == 2)
{
disk.transform.position = new Vector3(-15, 2.5f + Random.Range(-2, 2), 1);
}
else if (direction == 3)
{
disk.transform.position = new Vector3(15, 5 + Random.Range(-2, 2), 1);
attri.speedX *= -1;
attri.speedY *= -1;
}
else if (direction == 4)
{
disk.transform.position = new Vector3(15, 2.5f + Random.Range(-2, 2), 1);
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(0.4f, 0.4f, 0.4f);
Debug.Log("free disk");
used.Remove(disk);
free.Add(disk);
}
public void freeall()
{
while (used.Count != 0)
{
GameObject disk;
disk = used[0];
disk.SetActive(false);
disk.transform.position = new Vector3(0, 0, 0);
disk.transform.localScale = new Vector3(0.4f, 0.4f, 0.4f);
used.Remove(disk);
free.Add(disk);
}
}
}
本项目中由轮次管理员和计分管理员负责轮次控制以及分数计算两个方面,轮次管理员的设计更加接近于主控。除了主控由轮次管理员兼任以外,相较于之前项目的结构没有大变化,这里仅展示关键的两个controller代码。
轮次管理员RoundController
public class RoundController : MonoBehaviour, ISceneController, IUserAction
{
int round = 0;
int max_round = 5;
float timer = 0.5f;
GameObject disk;
DiskFactory factory;
public IActionManager actionManager;
public ScoreController scoreController;
public UserGUI userGUI;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
// 游戏未开始时不执行
if (userGUI.mode == 0) return;
// 根据运动模式附加动作管理员
if (userGUI.isKinematic == false)
{
actionManager = gameObject.GetComponent<PhysicalActionManager>() as IActionManager;
}
else
{
actionManager = gameObject.GetComponent<CCActionManager>() as IActionManager;
}
GetHit();
gameOver();
if (round > max_round)
{
return;
}
timer -= Time.deltaTime;
if (timer <= 0 && actionManager.RemainActionCount() == 0)
{
//从工厂中得到10个飞碟,为其加上动作
for (int i = 0; i < 10; ++i)
{
disk = factory.GetDisk(round);
actionManager.Fly(disk);
}
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<PhysicalActionManager>();
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);
//Return the ray's hit
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
scoreController.Record(hit.transform.gameObject);
hit.transform.gameObject.SetActive(false);
}
}
}
// 重新开始游戏
public void restart()
{
factory.freeall();
scoreController.restart();
round = 0;
timer = 0.5f;
userGUI.gameMessage = "";
userGUI.round = round;
}
}
计分管理员ScoreController
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;
}
public void restart()
{
score = 0;
userGUI.score = 0;
}
}
2.Action
根据游戏需要,动作方面根据两种动作模式设计了两套动作及对应管理员(运动学模式——CC,物理模式——Physical),两种动作模式都继承于SSAction和SSActionManager。同时和设计了IActionCallback和IActionManager作为连接其它控制器的桥梁和同一动作函数的多态。
CCActionManager
public class CCActionManager : SSActionManager, IActionCallback, IActionManager
{
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 as IActionManager;
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 override void MoveDisk(GameObject disk)
{
action = CCFlyAction.GetSSAction(disk.GetComponent<DiskAttributes>().speedX, disk.GetComponent<DiskAttributes>().speedY);
RunAction(disk, action, this);
}
public void Fly(GameObject disk)
{
MoveDisk(disk);
}
public int RemainActionCount()
{
return actions.Count;
}
}
CCFlyAction
public class CCFlyAction : SSAction
{
public float speedX;
public float speedY;
public Rigidbody rb;
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()
{
rb = gameObject.GetComponent<Rigidbody>();
rb.isKinematic = true;
}
// Update is called once per frame
public override void Update()
{
if (this.transform.gameObject.activeSelf == false)
{//飞碟已经被"销毁"
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 < 175 || vec3.y > Camera.main.pixelHeight + 100)
{
this.destroy = true;
this.callback.SSActionEvent(this);
return;
}
transform.position = transform.position + new Vector3(speedX * (7 + Random.Range(-7, 7)) * Time.deltaTime, speedY * (2 + Random.Range(-1, 1)) * Time.deltaTime, 0);
}
}
PhysicalActionManager
public class PhysicalActionManager : SSActionManager, IActionCallback, IActionManager
{
public RoundController sceneController;
public PhysicalFlyAction action;
public DiskFactory factory;
// Start is called before the first frame update
protected new void Start()
{
sceneController = (RoundController)SSDirector.getInstance().currentSceneController;
sceneController.actionManager = this as IActionManager;
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 override void MoveDisk(GameObject disk)
{
action = PhysicalFlyAction.GetSSAction(disk.GetComponent<DiskAttributes>().speedX);
RunAction(disk, action, this);
}
public void Fly(GameObject disk)
{
MoveDisk(disk);
}
public int RemainActionCount()
{
return actions.Count;
}
}
PhysicalFlyAction
public class PhysicalFlyAction : SSAction
{
public float speedX;
public Rigidbody rb;
public static PhysicalFlyAction GetSSAction(float x)
{
PhysicalFlyAction action = ScriptableObject.CreateInstance<PhysicalFlyAction>();
action.speedX = x;
return action;
}
// Start is called before the first frame update
public override void Start()
{
rb = gameObject.GetComponent<Rigidbody>();
rb.isKinematic = false;
}
// Update is called once per frame
public override void Update()
{
if (this.transform.gameObject.activeSelf == false)
{//飞碟已经被"销毁"
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)
{
this.destroy = true;
this.callback.SSActionEvent(this);
return;
}
gameObject.GetComponent<Rigidbody>().AddForce(speedX * (0.01f + Random.Range(-0.01f, 0.01f)), 0, 0, ForceMode.VelocityChange);
}
}
3.View
主要是游戏界面设计和玩家动作反馈。
IUserAction
public interface IUserAction
{
void gameOver();
void GetHit();
void restart();
}
UserGUI
public class UserGUI : MonoBehaviour
{
public int mode;
public int score;
public int round;
public string gameMessage;
public bool isKinematic = true;
private IUserAction action;
public GUIStyle bigStyle, smallStyle;//自定义字体格式
private int menu_width = Screen.width / 5, menu_height = Screen.width / 15;//主菜单每一个按键的宽度和高度
// Start is called before the first frame update
void Start()
{
mode = 0;
gameMessage = "";
action = SSDirector.getInstance().currentSceneController as IUserAction;
//大字体初始化
bigStyle = new GUIStyle();
bigStyle.normal.textColor = Color.black;
bigStyle.normal.background = null;
bigStyle.fontSize = 50;
bigStyle.alignment = TextAnchor.MiddleCenter;
//小字体初始化
smallStyle = new GUIStyle();
smallStyle.normal.textColor = Color.black;
smallStyle.normal.background = null;
smallStyle.fontSize = 20;
smallStyle.alignment = TextAnchor.MiddleCenter;
}
// Update is called once per frame
void Update()
{
}
void OnGUI()
{
GUI.skin.button.fontSize = 35;
switch (mode)
{
case 0:
mainMenu();
break;
case 1:
GameStart();
break;
}
}
void mainMenu()
{
GUI.Label(new Rect(Screen.width / 2 - menu_width * 0.5f, Screen.height * 0.1f, menu_width, menu_height), "Hit UFO", bigStyle);
bool button = GUI.Button(new Rect(Screen.width / 2 - menu_width * 0.5f, Screen.height * 3 / 7, menu_width, menu_height), "Start");
if (button)
{
mode = 1;
}
}
void GameStart()
{
GUI.Label(new Rect(Screen.width / 2 - 25, Screen.height / 1.5f - 100, 50, 200), gameMessage, bigStyle);
GUI.Label(new Rect(0, 0, 100, 50), "Score: " + score, smallStyle);
if(isKinematic)
GUI.Label(new Rect(0, 50, 200, 50), "Mode: Kinematic", smallStyle);
else
GUI.Label(new Rect(0, 50, 200, 50), "Mode: Not Kinematic", smallStyle);
GUI.Label(new Rect(Screen.width - 100, 0, 100, 50), "Round: " + round + "/5", smallStyle);
bool kchange = GUI.Button(new Rect(Screen.width / 2 - menu_width * 1.2f, Screen.height - menu_height, menu_width * 2.4f, menu_height), "Kinematic/Not Kinematic");
if (kchange)
{
isKinematic = !isKinematic;
}
bool rs = GUI.Button(new Rect(Screen.width - menu_width, Screen.height - menu_height, menu_width, menu_height), "Restart");
if (rs)
{
action.restart();
}
}
}
四、实机演示
Bilibili:吾射不亦精乎?Unity3D小游戏 打飞碟_哔哩哔哩bilibili