與遊戲世界互動
- 編寫一個簡單的滑鼠打飛碟(Hit UFO)遊戲
-
- 内容要求
-
- 遊戲内容要求
- 遊戲設計要求
- 遊戲設計
-
- 項目分析與結構
- 對象預制
- 實作過程解讀
- 設計展示
- 編寫一個簡單的自定義 Component (選做)
-
- 要求
- 實作
編寫一個簡單的滑鼠打飛碟(Hit UFO)遊戲
項目傳送門: https://github.com/lurui7/3D-Game/tree/master/Hit%20UFO
内容要求
遊戲内容要求
- 遊戲有 n 個 round,每個 round 都包括10 次 trial;
- 每個 trial 的飛碟的色彩、大小、發射位置、速度、角度、同時出現的個數都可能不同。它們由該 round 的 ruler 控制;
- 每個 trial 的飛碟有随機性,總體難度随 round 上升;
- 滑鼠點中得分,得分規則按色彩、大小、速度不同計算,規則可自由設定。
遊戲設計要求
- 使用帶緩存的工廠模式管理不同飛碟的生産與回收,該工廠必須是場景單執行個體的!具體實作見參考資源 Singleton 模闆類
- 盡可能使用前面 MVC 結構實作人機互動與遊戲模型分離
遊戲設計
項目分析與結構
首先考慮到這次項目與以往最大的不同之處在于工廠模式的使用,需要工廠方法 + 單執行個體 + 對象池 一起使用。根據老師給出的設計如下:
得到如下的項目結構:
對象預制
實作過程解讀
首先就是SSDirector.cs
功能是控制場景切換,實作與上次相比沒有任何變化
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSDirector : System.Object {
private static SSDirector _instance;
public ISceneController CurrentScenceController { get; set; }
public static SSDirector GetInstance() {
if (_instance == null) {
_instance = new SSDirector();
}
return _instance;
}
}
接口類Interface.cs
定義接口以及回調函數,實作不同類的連結。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneController {
void LoadResources();
}
public interface IUserAction {
void Hit(Vector3 pos);
float GetScore();
int GetRound();
int GetTrial();
void GameOver();
void ReStart();
}
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);
}
單執行個體的實作Singleton.cs
場景單執行個體類,當使用執行個體時,隻需要通過Singleton<\YourMonoType>.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;
}
}
}
Disk.cs
飛碟對象的定義
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Disk : MonoBehaviour {
public int type = 1;
public int score = 1;
public Color color = Color.white;
}
DiskFactory.cs
使用工廠模式,負責生産和回收飛碟。
GetDisk:用于生産飛碟,首先從free空閑隊列中查找是否有可用的飛碟,如果沒有則建立一個飛碟。
FreeDisk:FreeDisk用于釋放飛碟,将飛碟從used隊列中移除并添加到free隊列中。
Reset:定義一個reset方法,提供給外部類使用,以便于在遊戲開始或者重新開始時初始化。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 工廠
public class DiskFactory : MonoBehaviour {
private List<Disk> used = new List<Disk>();
private List<Disk> free = new List<Disk>();
public GameObject GetDisk(int type) {
GameObject disk_prefab = null;
//尋找空閑飛碟,如果無空閑飛碟則重新執行個體化飛碟
if (free.Count>0) {
for(int i = 0; i < free.Count; i++) {
if (free[i].type == type) {
disk_prefab = free[i].gameObject;
free.Remove(free[i]);
break;
}
}
}
if(disk_prefab == null) {
if(type == 1) {
disk_prefab = Instantiate(
Resources.Load<GameObject>("Prefabs/disk1"),
new Vector3(0, -10f, 0), Quaternion.identity);
}
else if (type == 2) {
disk_prefab = Instantiate(
Resources.Load<GameObject>("Prefabs/disk2"),
new Vector3(0, -10f, 0), Quaternion.identity);
}
else {
disk_prefab = Instantiate(
Resources.Load<GameObject>("Prefabs/disk3"),
new Vector3(0, -10f, 0), Quaternion.identity);
}
disk_prefab.GetComponent<Renderer>().material.color = disk_prefab.GetComponent<Disk>().color;
}
used.Add(disk_prefab.GetComponent<Disk>());
disk_prefab.SetActive(true);
return disk_prefab;
}
public void FreeDisk() {
for(int i=0; i<used.Count; i++) {
if (used[i].gameObject.transform.position.y <= -10f) {
free.Add(used[i]);
used.Remove(used[i]);
}
}
}
public void Reset() {
FreeDisk();
}
}
SSAction.cs, SSActionManager.cs, SequenceAction
SSAction.cs是動作類的基類,其中gameObject為動作作用的實體對象。callback是回調接口,當動作類需要向别的類傳遞資訊時,就通過ISSActionCallback接口來實作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/*動作基類*/
public class SSAction : ScriptableObject {
public bool enable = true; //是否進行
public bool destroy = false; //是否删除
public GameObject gameobject; //動作對象
public Transform transform; //動作對象的transform
public ISSActionCallback callback; //回調函數
/*防止使用者自己new對象*/
protected SSAction() { }
public virtual void Start() {
throw new System.NotImplementedException();
}
public virtual void Update() {
throw new System.NotImplementedException();
}
}
SSActionManager.cs是動作管理類的基類,作為動作生成、運作與銷毀的管理者。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/*動作管理基類*/
public class SSActionManager : MonoBehaviour, ISSActionCallback {
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>(); //動作字典
private List<SSAction> waitingAdd = new List<SSAction>(); //等待執行的動作清單
private List<int> waitingDelete = new List<int>(); //等待删除動作的key的清單
protected void Update() {
//擷取動作執行個體将等待執行的動作加入字典并清空待執行清單
foreach (SSAction ac in waitingAdd) {
actions[ac.GetInstanceID()] = ac;
}
waitingAdd.Clear();
//對于字典中每一個pair,看是執行還是删除
foreach (KeyValuePair<int, SSAction> kv in actions) {
SSAction ac = kv.Value;
if (ac.destroy) {
waitingDelete.Add(ac.GetInstanceID());
}
else if (ac.enable) {
ac.Update();
}
}
//删除所有已完成的動作并清空待删除清單
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();
}
public void SSActionEvent(
SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null) {
}
}
SequenceAction為組合動作類,即把一個動作序列按照合适的順序執行。
這三個部分與上次實作的牧師與魔鬼是完全一緻的。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SequenceAction : SSAction, ISSActionCallback
{
public List<SSAction> sequence; //動作的清單
public int repeat = -1; //-1就是無限循環做組合中的動作
public int start = 0; //目前做的動作的索引
public static SequenceAction GetSSAcition(int repeat, int start, List<SSAction> sequence)
{
SequenceAction action = ScriptableObject.CreateInstance<SequenceAction>();
action.repeat = repeat;
action.sequence = sequence;
action.start = start;
return action;
}
public override void Update()
{
if (sequence.Count == 0) return;
if (start < sequence.Count)
{
sequence[start].Update();
}
}
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null)
{
source.destroy = false;
this.start++;
if (this.start >= sequence.Count)
{
this.start = 0;
if (repeat > 0) repeat--;
if (repeat == 0)
{
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
}
public override void Start()
{
foreach (SSAction action in sequence)
{
action.gameobject = this.gameobject;
action.transform = this.transform;
action.callback = this;
action.Start();
}
}
void OnDestroy()
{
}
}
DiskFlyAction.cs
這是實作飛碟運動的核心部分,通過給定的初始位置,初始角度,初始力來控制飛碟的運動。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 模拟飛行
public class DiskFlyAction : SSAction {
public float gravity = -1; //向下的加速度
private Vector3 start_vector; //初速度向量
private Vector3 gravity_vector = Vector3.zero; //加速度的向量,初始時為0
private Vector3 current_angle = Vector3.zero; //目前時間的歐拉角
private float time; //已經過去的時間
private DiskFlyAction() { }
public static DiskFlyAction GetSSAction(int lor, float angle, float power) {
//初始化物體将要運動的初速度向量
DiskFlyAction action = CreateInstance<DiskFlyAction>();
if (lor == -1) {
action.start_vector = Quaternion.Euler(new Vector3(0, 0, -angle)) * Vector3.left * power;
}
else {
action.start_vector = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * power;
}
return action;
}
public override void Update() {
//計算物體的向下的速度,v=at
time += Time.fixedDeltaTime;
gravity_vector.y = gravity * time;
//位移模拟
transform.position += (start_vector + gravity_vector) * Time.fixedDeltaTime;
current_angle.z = Mathf.Atan((start_vector.y + gravity_vector.y) / start_vector.x) * Mathf.Rad2Deg;
transform.eulerAngles = current_angle;
//如果物體y坐标小于-1,動作就做完了
if (this.transform.position.y < -1) {
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
public override void Start() { }
}
FlyActionManager.cs
飛碟動作管理類,通過DiskFly方法控制飛碟飛行。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FlyActionManager : SSActionManager {
public DiskFlyAction fly;
public FirstController scene_controller;
protected void Start() {
scene_controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
scene_controller.action_manager = this;
}
//控制飛碟飛行
public void DiskFly(GameObject disk, float angle, float power) {
int lor = 1;
if (disk.transform.position.x > 0) lor = -1;
fly = DiskFlyAction.GetSSAction(lor, angle, power);
this.RunAction(disk, fly, this);
}
}
分數記錄ScoreRecorder.cs
記錄分數,每次集中飛碟就加分(三種飛碟分數不一樣)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/*記錄分數*/
public class ScoreRecorder : MonoBehaviour {
private float score;
void Start () {
score = 0;
}
public void Record(GameObject disk) {
score += disk.GetComponent<Disk>().score;
}
public float GetScore() {
return score;
}
public void Reset() {
score = 0;
}
}
核心控制部分FirstControllor.cs
場景控制器,負責遊戲主要邏輯。
包括檢測滑鼠點選;
發送飛碟;
遊戲輪次及規則調整;
控制啟動與結束。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstController : MonoBehaviour, ISceneController, IUserAction {
public FlyActionManager action_manager;
public DiskFactory disk_factory;
public UserGUI user_gui;
public ScoreRecorder score_recorder;
private int round = 1;
private int trial = 0;
//private float speed = 1f;
private bool running = false;
void Start () {
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
disk_factory = Singleton<DiskFactory>.Instance;
score_recorder = Singleton<ScoreRecorder>.Instance;
action_manager = gameObject.AddComponent<FlyActionManager>() as FlyActionManager;
user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;
}
int count = 0;
//調整輪次,以及根據輪次修改發射規則
void Update () {
if(running) {
count++;
if (Input.GetButtonDown("Fire1")) {
Vector3 pos = Input.mousePosition;
Hit(pos);
}
switch (round) {
case 1: {
if (count >= 150) {
count = 0;
SendDisk(1);
trial += 1;
if (trial == 10) {
round += 1;
trial = 0;
}
}
break;
}
case 2: {
if (count >= 100) {
count = 0;
if (trial % 2 == 0) SendDisk(1);
else SendDisk(2);
trial += 1;
if (trial == 10) {
round += 1;
trial = 0;
}
}
break;
}
case 3: {
if (count >= 50) {
count = 0;
if (trial % 3 == 0) SendDisk(1);
else if(trial % 3 == 1) SendDisk(2);
else SendDisk(3);
trial += 1;
if (trial == 10) {
running = false;
}
}
break;
}
default:break;
}
disk_factory.FreeDisk();
}
}
public void LoadResources() {
disk_factory.GetDisk(round);
disk_factory.FreeDisk();
}
//發射飛碟
private void SendDisk(int type) {
//從工廠中拿一個飛碟
GameObject disk = disk_factory.GetDisk(type);
//飛碟位置
float ran_y = 0;
float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
//飛碟初始所受的力和角度
float power = 0;
float angle = 0;
if (type == 1) {
ran_y = Random.Range(1f, 5f);
power = Random.Range(5f, 20f);
angle = Random.Range(25f,30f);
}
else if (type == 2) {
ran_y = Random.Range(2f, 3f);
power = Random.Range(10f, 20f);
angle = Random.Range(15f, 17f);
}
else {
ran_y = Random.Range(5f, 6f);
power = Random.Range(15f, 20f);
angle = Random.Range(10f, 17f);
}
disk.transform.position = new Vector3(ran_x*16f, ran_y, 0);
action_manager.DiskFly(disk, angle, power);
}
//檢測射線與飛碟是否碰撞,如碰撞則計分并回收飛碟
public void Hit(Vector3 pos) {
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
for (int i = 0; i < hits.Length; i++) {
RaycastHit hit = hits[i];
if (hit.collider.gameObject.GetComponent<Disk>() != null) {
score_recorder.Record(hit.collider.gameObject);
hit.collider.gameObject.transform.position = new Vector3(0, -10, 0);
}
}
}
public float GetScore() {
return score_recorder.GetScore();
}
public int GetRound() {
return round;
}
public int GetTrial() {
return trial;
}
//重新開始
public void ReStart() {
running = true;
score_recorder.Reset();
disk_factory.Reset();
round = 1;
trial = 1;
//speed = 2f;
}
//遊戲結束
public void GameOver() {
running = false;
}
}
最後就是GUI了
這一部分與上次的相比并沒有多大的變化,基本沿用了牧師與魔鬼的UI界面設計。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour {
private IUserAction action;
//每個GUI的style
GUIStyle bold_style = new GUIStyle();
GUIStyle text_style = new GUIStyle();
GUIStyle over_style = new GUIStyle();
private bool game_start = false;
//控制規則顯示
bool isShow = false;
void Start () {
action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
}
void OnGUI () {
bold_style.normal.textColor = new Color(1, 0, 0);
bold_style.fontSize = 16;
text_style.normal.textColor = new Color(0, 0, 0, 1);
text_style.fontSize = 16;
over_style.normal.textColor = new Color(1, 0, 0);
over_style.fontSize = 25;
GUIStyle button_style = new GUIStyle("button")
{
fontSize = 15
};
if (GUI.Button(new Rect(10, 10, 60, 30), "Rule", button_style))
{
if (isShow)
isShow = false;
else
isShow = true;
}
if(isShow)
{
GUI.Label(new Rect(Screen.width / 2 - 400 , 70, 100, 50), "點選Start開始遊戲", text_style);
GUI.Label(new Rect(Screen.width / 2 - 400, 90, 250, 50), "遊戲過程中滑鼠左鍵為hit", text_style);
GUI.Label(new Rect(Screen.width / 2 - 400, 110, 250, 50), "随着時間的推移,難度會上升", text_style);
GUI.Label(new Rect(Screen.width / 2 - 400, 130, 250, 50), "上吧!!!", text_style);
}
if (game_start) {
GUI.Label(new Rect(Screen.width - 150, 5, 200, 50), "Score:"+ action.GetScore().ToString(), text_style);
GUI.Label(new Rect(100, 5, 50, 50), "Round:" + action.GetRound().ToString(), text_style);
GUI.Label(new Rect(180, 5, 50, 50), "Trial:" + action.GetTrial().ToString(), text_style);
if (action.GetRound() == 3 && action.GetTrial() == 10) {
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 100, 100, 100), "GAME OVER", over_style);
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 50, 50, 50), "YOUR SCORE: " + action.GetScore().ToString(), over_style);
if (GUI.Button(new Rect(Screen.width / 2 - 20, Screen.height / 2, 100, 50), "RESTART")) {
action.ReStart();
return;
}
action.GameOver();
}
}
else {
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 100, 100, 100), "Hit UFO", over_style);
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2, 100, 50), "START")) {
game_start = true;
action.ReStart();
}
}
}
}
設計展示
這裡我使用了Fantasy Skybox FREE包來建構場景,需要把FirstController,DiskFactory,ScoreRecorder,FlyActionManager,UserGUI挂載到Camera上就可以運作遊戲了,示範效果如下:
編寫一個簡單的自定義 Component (選做)
要求
- 用自定義元件定義幾種飛碟,做成預制
- 參考官方腳本手冊 https://docs.unity3d.com/ScriptReference/Editor.html
- 實作自定義元件,編輯并賦予飛碟一些屬性
實作
在前面Disk類的基礎上,可以建立一個DiskEditor類,它首先序列化飛碟屬性,如顔色,大小,分數等;
然後可以在其中給它添加一些自定義屬性,直接應用就可以實作自定義的Component了。
using UnityEngine;
using UnityEditor;
using System.Collections;
[CustomEditor(typeof(Disk))]
[CanEditMultipleObjects]
public class DiskEditor : Editor
{
SerializedProperty score;
SerializedProperty color;
SerializedProperty scale;
void OnEnable()
{
score = serializedObject.FindProperty("score");
color = serializedObject.FindProperty("color");
scale = serializedObject.FindProperty("scale");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.IntSlider(score, 0, 5, new GUIContent("score"));
serializedObject.ApplyModifiedProperties();
}
}