与游戏世界交互

  • 3D游戏设计第五次作业
  • 前言
  • 简单的打飞碟小游戏
  • 说明
  • 设计图
  • 代码分析
  • 导演类Director
  • 接口场景类ISceneController
  • 接口类IUserAction
  • 飞碟数据类DiskData
  • 飞碟工厂类DiskFactory
  • 所有动作的基础类SSAction
  • 飞碟飞行动作类CCFlyAction
  • 组合动作管理类SSActionManager
  • 动作事件接口类ISSActionCallback
  • 事件管理类CCActionManager
  • 最高级的控制类FirstSceneController
  • 用户界面类UserGUI
  • 模板类Singleton
  • 记分员ScoreRecorder
  • 编写一个简单的自定义 Component (选做)
  • 天空盒设置
  • 成品展示
  • 源码仓库
  • 感悟


3D游戏设计第五次作业

前言

这是中山大学2020年3D游戏设计的第五次作业,如有错误,请指正,感谢您的阅读。

简单的打飞碟小游戏

说明

  • 游戏规则
  • 游戏有两个模式,时间模式和生存模式
  • 时间模式:在60s内能点击的飞碟数,每点中10个飞碟升级一次round
  • 生存模式:共有五点生命值,每漏掉一个飞碟减一生命值,每点中10个飞碟升级一次round
  • 注:两种模式下的升级方式并不同,时间模式飞碟数比较多,而生存模式相对简单(因为有血量限制)
  • 游戏共有10个round,每打中十次升级升级后速度更快、数量更多
  • 第二个round才会有黄色飞碟,第五个round才会有红色飞碟,黄色加2分,红色加3分
  • 游戏要求
  • 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
  • 近可能使用前面 MVC 结构实现人机交互与游戏模型分离

设计图

Unity 实现可交互水波纹_Unity 实现可交互水波纹


MVC设计图之前已给出,这里就不再给出具体设计图

代码分析

导演类Director

单例模式,继承System.Object(会不被Unity内存管理,但所有Scene都能访问到它),主要控制场景切换
这里直接给出导演类的实例

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;
    }
}
接口场景类ISceneController

负责指明具体实现的场景类要实现的方法,而且便于更多的类能通过接口来访问场景类,由FirstSceneController具体场景实现类来实现。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface ISceneController
{
    void LoadResources();                                  
}
接口类IUserAction

负责指明由用户行为引发的变化的方法,由FirstSceneController这个最高级的控制类来实现。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface IUserAction
{
    void Hit(Vector3 pos);
    int GetScore();
    void GameOver();
    void ReStart();
    void BeginGame();
    float GetTime();
    int GetLevel();
}
飞碟数据类DiskData

说明当前飞碟的状态,用于描述飞碟。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DiskData : MonoBehaviour
{
    public int score = 1;
    public Color color = Color.white;
    public Vector3 direction; 
    public Vector3 scale = new Vector3( 1 ,0.25f, 1);
}
飞碟工厂类DiskFactory

用于制造和销毁飞碟的工厂
首先创建变量,主要包括预制体、一个正在使用列表以及一个空闲列表

public GameObject disk_prefab = null;
    private List<DiskData> used = new List<DiskData>();
    private List<DiskData> free = new List<DiskData>();

获取飞碟即如果有空闲直接使用,没有则创建的过程

public GameObject GetDisk(int round)
    {
        float start_y = -10f;
        string tag;
        disk_prefab = null;
        int choice;
        choice = GetDiskNum(round, 3, 6, 8);
        Debug.Log(choice);
        tag = SetTag(3, 6, choice);
        Debug.Log(tag);
        for (int i = 0; i < free.Count; i++)
        {
            if (free[i].tag == tag)
            {
                disk_prefab = free[i].gameObject;
                free.Remove(free[i]);
                break;
            }
        }
        if (disk_prefab == null)
        {
            NewDisk(start_y, tag);
            float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
            disk_prefab.GetComponent<Renderer>().material.color = disk_prefab.GetComponent<DiskData>().color;
            disk_prefab.GetComponent<DiskData>().direction = new Vector3(ran_x, start_y, 0);
            disk_prefab.transform.localScale = disk_prefab.GetComponent<DiskData>().scale;
        }
        used.Add(disk_prefab.GetComponent<DiskData>());
        return disk_prefab;
    }

最后释放飞碟的过程是一个由正在使用转为空闲的过程

public void FreeDisk(GameObject disk)
    {
        for(int i = 0;i < used.Count; i++)
        {
            if (disk.GetInstanceID() == used[i].gameObject.GetInstanceID())
            {
                used[i].gameObject.SetActive(false);
                free.Add(used[i]);
                used.Remove(used[i]);
                break;
            }
        }
    }
所有动作的基础类SSAction

用于规定所有动作的基础规范,继承ScriptableObject。

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; 
    public ISSActionCallback callback;

    protected SSAction() { }                        
    public virtual void Start()
    {
        throw new System.NotImplementedException();
    }
    public virtual void Update()
    {
        throw new System.NotImplementedException();
    }
}
飞碟飞行动作类CCFlyAction

主要实现飞碟飞行,通过设置加速度来让飞碟加速下滑,通过角度和初速度实现飞碟的不同飞行轨迹,具体实现在FirstViewController中进行

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CCFlyAction : SSAction
{
    public float gravity = -7;
    private Vector3 start_vector;
    private Vector3 gravity_vector = Vector3.zero;
    private float time;
    private Vector3 current_angle = Vector3.zero;

    private CCFlyAction() { }
    public static CCFlyAction GetSSAction(Vector3 direction, float angle, float power)
    {
        CCFlyAction action = CreateInstance<CCFlyAction>();
        if (direction.x == -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()
    {
        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;

        if (this.transform.position.y < -10)
        {
            this.destroy = true;
            this.callback.SSActionEvent(this);      
        }
    }

    public override void Start() { }
}
组合动作管理类SSActionManager

用于管理一系列的动作,负责创建和销毁它们。具体介绍可以看上次的博客。

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>();            

    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)
            {
                ac.Update();
            }
        }

        foreach (int key in waitingDelete)
        {
            SSAction ac = actions[key];
            actions.Remove(key);
            DestroyObject(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)
    {
    }
}
动作事件接口类ISSActionCallback

定义了事件处理的接口,事件管理器必须实现它。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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);
}
事件管理类CCActionManager

继承了SSActionManager,实现了ISSActionCallback,负责事件的处理。

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();
        }
    }
最高级的控制类FirstSceneController

负责底层数据与用户操作的GUI的交互,实现ISceneControl和IUserAction,由于这里代码比较长,故不放出全部代码,仅看Update的代码

void Update ()
    {
        if(game_start)
        {
            if(time>0 &&user_gui.getMode()==1)
                time = time - Time.deltaTime;
            if (time < 0)
            {
                time = 0;
                game_over = true;
            }
            if (game_over)
            {
                CancelInvoke("LoadResources");
                game_start = false;
                for(int i = 0; i < disk_notshot.Count; i++)
                {
                    disk_factory.FreeDisk(disk_notshot[i]);
                    disk_notshot.Remove(disk_notshot[i]);
                }
                    disk_queue.Clear();
            }
            if (!playing_game)
            {
                if (user_gui.getMode() == 1) {
                    for (int i = 0; i < 5; i++)
                        LoadResources();
                    InvokeRepeating("LoadResources", 0.1f, speed / 6);
                }
                else
                {
                    InvokeRepeating("LoadResources", 0.1f, speed / 3);
                }
                playing_game = true;
            }
                SendDisk();
            if (score_recorder.score >= score_round[now_round])
            {
                if (user_gui.getMode() == 1)
                    LevelUpMode1(++now_round);
                else
                    LevelUpMode2(++now_round);
                Debug.Log("round:" + now_round);
            }
        }
    }
用户界面类UserGUI

负责生成界面交于用户操作
这里为节省空间不放出设置UI的代码,仅放出开始游戏相关的调用

if (GUI.Button(new Rect(Screen.width / 2 - 70, 300, 150, 75), "计时模式"))
                {
                    action.ReStart();
                    action.BeginGame();
                    mode = 1;
                    return;
                }
                if (GUI.Button(new Rect(Screen.width / 2 - 70, 400, 150, 75), "生存模式"))
                {
                    action.ReStart();
                    action.BeginGame();
                    life = 5;
                    mode = 2;
                    return;
                }
模板类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;
        }
    }
}
记分员ScoreRecorder

用于计分的类,非常简单,不再赘述

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ScoreRecorder : MonoBehaviour
{
    public int score;
    void Start ()
    {
        score = 0;
    }
    public void Record(GameObject disk)
    {
        int temp = disk.GetComponent<DiskData>().score;
        score = temp + score;
    }
    public void Reset()
    {
        score = 0;
    }
}

编写一个简单的自定义 Component (选做)

这里用自定义组件定义三种飞碟,做成预制
三种飞碟挂载的脚本不同,其余设置基本完全相同
我们用一个脚本来挂载到每个预制体,脚本如下

using UnityEngine;
using UnityEditor;
using System.Collections;
[CustomEditor(typeof(DiskData))]
[CanEditMultipleObjects]
public class MyDEditor : 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"));
  
        if (!score.hasMultipleDifferentValues)
        {
            ProgressBar(score.intValue / 5f, "score");
        }
        EditorGUILayout.PropertyField(color);
        EditorGUILayout.PropertyField(scale);
        serializedObject.ApplyModifiedProperties();
    }
    private void ProgressBar(float value, string label)
    {
        Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
        EditorGUI.ProgressBar(rect, value, label);
        EditorGUILayout.Space();
    }
}

脚本具体配置如下所示:

第一个

Unity 实现可交互水波纹_c#_02


第二个

Unity 实现可交互水波纹_游戏_03


第三个

Unity 实现可交互水波纹_sed_04

天空盒设置

相机的天空盒设置如下所示

Unity 实现可交互水波纹_sed_05


使用的资源如下

Unity 实现可交互水波纹_游戏_06

成品展示

计时模式:

Unity 实现可交互水波纹_游戏_07


生存模式:

Unity 实现可交互水波纹_游戏_08

源码仓库

打飞碟

感悟

本次实验为一个全新的打飞碟游戏,利用MVC架构和工厂的生成方式进行设计。通过MVC架构我们可以很容易的写好一个基础的框架,而通过一个工厂的设计,让我后续的预制可以很轻松的加入代码中,而且会提高游戏的性能。同时,在之后加入两个模式的设计,因为有十分好的架构也变得十分容易。在编程过程中经常出现一些意外的错误,但是因为我们使用的架构,通过在特定的位置打印一下状态也可以很轻松的解决。
最后,这次游戏设计中收获最大的就是学会用工厂对象实现了预制体实例化后的重用,提高了游戏性能。如果合理的运用,那会对以后的设计有锦上添花的作用。