天天看點

3D遊戲-作業五-與遊戲世界互動編寫一個簡單的滑鼠打飛碟(Hit UFO)遊戲編寫一個簡單的自定義 Component (選做)

與遊戲世界互動

  • 編寫一個簡單的滑鼠打飛碟(Hit UFO)遊戲
    • 内容要求
      • 遊戲内容要求
      • 遊戲設計要求
    • 遊戲設計
      • 項目分析與結構
      • 對象預制
      • 實作過程解讀
    • 設計展示
  • 編寫一個簡單的自定義 Component (選做)
    • 要求
    • 實作

編寫一個簡單的滑鼠打飛碟(Hit UFO)遊戲

項目傳送門: https://github.com/lurui7/3D-Game/tree/master/Hit%20UFO

内容要求

遊戲内容要求

  1. 遊戲有 n 個 round,每個 round 都包括10 次 trial;
  2. 每個 trial 的飛碟的色彩、大小、發射位置、速度、角度、同時出現的個數都可能不同。它們由該 round 的 ruler 控制;
  3. 每個 trial 的飛碟有随機性,總體難度随 round 上升;
  4. 滑鼠點中得分,得分規則按色彩、大小、速度不同計算,規則可自由設定。

遊戲設計要求

  1. 使用帶緩存的工廠模式管理不同飛碟的生産與回收,該工廠必須是場景單執行個體的!具體實作見參考資源 Singleton 模闆類
  2. 盡可能使用前面 MVC 結構實作人機互動與遊戲模型分離

遊戲設計

項目分析與結構

首先考慮到這次項目與以往最大的不同之處在于工廠模式的使用,需要工廠方法 + 單執行個體 + 對象池 一起使用。根據老師給出的設計如下:

3D遊戲-作業五-與遊戲世界互動編寫一個簡單的滑鼠打飛碟(Hit UFO)遊戲編寫一個簡單的自定義 Component (選做)

得到如下的項目結構:

3D遊戲-作業五-與遊戲世界互動編寫一個簡單的滑鼠打飛碟(Hit UFO)遊戲編寫一個簡單的自定義 Component (選做)

對象預制

3D遊戲-作業五-與遊戲世界互動編寫一個簡單的滑鼠打飛碟(Hit UFO)遊戲編寫一個簡單的自定義 Component (選做)

實作過程解讀

首先就是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上就可以運作遊戲了,示範效果如下:

3D遊戲-作業五-與遊戲世界互動編寫一個簡單的滑鼠打飛碟(Hit UFO)遊戲編寫一個簡單的自定義 Component (選做)
3D遊戲-作業五-與遊戲世界互動編寫一個簡單的滑鼠打飛碟(Hit UFO)遊戲編寫一個簡單的自定義 Component (選做)
3D遊戲-作業五-與遊戲世界互動編寫一個簡單的滑鼠打飛碟(Hit UFO)遊戲編寫一個簡單的自定義 Component (選做)
3D遊戲-作業五-與遊戲世界互動編寫一個簡單的滑鼠打飛碟(Hit UFO)遊戲編寫一個簡單的自定義 Component (選做)

編寫一個簡單的自定義 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();
	}
}
           

繼續閱讀