《3D游戏编程与设计》第五次作业

编写一个简单的树表打飞碟(Hit UFO)游戏

  • 游戏内容要求
    1.游戏有 n 个 round,每个 round 都包括10 次 trial。
    2.每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制。
    3.每个 trial 的飞碟有随机性,总体难度随 round 上升。
    4.鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。

  • 游戏设计要求
    1.使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类。
    2.尽可能使用前面 MVC 结构实现人机交互与游戏模型分离。

具体代码及演示视频见项目地址

根据项目可以设计整体框架如下:
1.导演、场记相关:SSDirector单例模式、ISceneController场景接口、FirstController游戏控制器。
2.用户相关:IUserAction用户动作接口、UserGUI用户界面。
3.飞碟生成相关:DiskData数据类型、DiskFactory工厂模式。
4.动作管理相关:SSActionManager动作管理基类、IActionManager飞行动作管理的接口、CCActionManager运动学动作管理。
4.飞行动作相关:SSAction动作基类、ISSActionCallBack动作结束的回调接口、CCFlyAction动作实现。
5.辅助类:Singleton单例模板、ScoreRecorder记分员。


Scripts总览

天空盒

预制

脚本

(一)Actions动作与动作管理器

1.SSAction动作基类

public class SSAction : ScriptableObject
{
    public bool enable = true;                      //是否可进行
    public bool destroy = false;                    //是否已完成

    public GameObject gameobject;                   //动作对象
    public Transform transform;                     //动作对象的transform
    public ISSActionCallback callback;              //回调函数

    protected SSAction() { }

    public virtual void Start()
    {
        throw new System.NotImplementedException();
    }

    public virtual void Update()
    {
        throw new System.NotImplementedException();
    }
}

2.SSActionManager 动作管理类基类

public class SSActionManager : MonoBehaviour
{
    private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();    //动作字典
    private List<int> waitingDelete = new List<int>();                              //等待删除的key列表   
    private List<SSAction> waitingAdd = new List<SSAction>();                       //等待执行的动作列表             

    protected void Update()
    {
        //将等待执行的动作加入字典并清空待执行列表
        foreach (SSAction ac in waitingAdd)
        {
            actions[ac.GetInstanceID()] = ac;
        }
        waitingAdd.Clear();

        //遍历字典中的动作,分执行/删除
        foreach (KeyValuePair<int, SSAction> kv in actions)
        {
            SSAction ssac = kv.Value;
            if (ssac.enable)
            {
                ssac.Update();
            }
            else if (ssac.destroy)
            {
                waitingDelete.Add(ssac.GetInstanceID());
            }
        }

        //删除所有已完成的动作并清空待删除列表
        foreach (int key in waitingDelete)
        {
            SSAction ssac = actions[key];
            actions.Remove(key);
            Object.Destroy(ssac);
        }
        waitingDelete.Clear();
    }

    //外界只需要调用动作管理类的RunAction函数即可完成动作。
    public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager)
    {
        action.gameobject = gameobject;
        action.transform = gameobject.transform;
        action.callback = manager;
        waitingAdd.Add(action);
        action.Start();
    }
}

3.CCFlyAction 飞碟动作类

public class CCFlyAction : SSAction
{
    float speed;
    Vector3 direction;

    public static CCFlyAction GetSSAction(Vector3 dir, float speed)
    {
        CCFlyAction tmp = ScriptableObject.CreateInstance<CCFlyAction>();
        tmp.speed = speed;
        tmp.direction = dir;
        return tmp;
    }

    public override void Start()
    {
        gameobject.GetComponent<Rigidbody>().isKinematic = false;
        //为物体增加水平初速度,否则会由于受到重力严重影响运动状态。
        gameobject.GetComponent<Rigidbody>().velocity = speed * direction;
    }

    public override void Update()
    {
        //动作运行
        transform.Translate(direction * speed * Time.deltaTime);
        //判断飞碟是否落地 
        if (this.transform.position.y < -10)
        {
            this.destroy = true;
            this.enable = false;
            this.callback.SSActionEvent(this);
        }
    }
}
  1. CCActionManager 动作管理类
    动作结束时会调用动作管理者实现的回调函数,即IActionCallback接口中的SSActionEventType,动作管理者将动作绑定的游戏对象(飞碟)销毁。
public class CCActionManager : SSActionManager, ISSActionCallback, IActionManager
{
    public CCFlyAction Action;
    public FirstController controller;
    public void Start()
    {
        controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
    }

    public void Fly(GameObject disk, float speed, Vector3 direction)
    {
        Action = CCFlyAction.GetSSAction(direction, speed);
        RunAction(disk, Action, this);
    }

    //回调函数
    public void SSActionEvent(SSAction source,
        SSActionEventType events = SSActionEventType.Competed,
        int intParam = 0,
        string strParam = null,
        Object objectParam = null)
    {
        controller.diskFactory.FreeDisk(source.gameobject);//释放资源
    }
}

5.IActionCallback 事件回调接口

public enum SSActionEventType : int { Started, Competed }
public interface ISSActionCallback
{
    //回调函数
    void SSActionEvent(SSAction source,
        SSActionEventType events = SSActionEventType.Competed,
        int intParam = 0,
        string strParam = null,
        Object objectParam = null);
}

6.IActionManager动作管理接口
接口只定义一个函数,便于采用Adapter模式适应不同的接口

public interface IActionManager
{
    void Fly(GameObject disk, float speed, Vector3 direction);
}



(二)Disk相关
1.DiskFactory 飞碟工厂
GetDisk被主控制器调用,round会影响所生产的飞碟的速度、大小等属性,round越高游戏难度越大。
有两个列表use和free,存放的是飞碟属性(包括分数、速度)。
飞碟初始位置、颜色、速度大小以及方向都随机。

public class DiskFactory : MonoBehaviour
{
    public GameObject disk_prefab;

    private List<DiskData> use;    //已使用的飞碟
    private List<DiskData> free;    //空闲的飞碟

    void Start()
    {
        use = new List<DiskData>();
        free = new List<DiskData>();
        disk_prefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/UFO"), Vector3.zero, Quaternion.identity);
        disk_prefab.SetActive(false);
    }

    //获取飞碟
    public GameObject GetDisk(int round)
    {
        GameObject disk;

        //如果有空闲的飞碟,则直接使用。否则生成新的飞碟
        if (free.Count > 0)
        {
            disk = free[0].gameObject;
            free.Remove(free[0]);
        }
        else
        {
            disk = GameObject.Instantiate<GameObject>(disk_prefab, Vector3.zero, Quaternion.identity);
            disk.AddComponent<DiskData>();
        }
        disk.gameObject.name = "Disk";

        //生成飞碟的规则
        float base_speed = 3 + round * 0.8f;        //round越大,速度基数越大,难度越高
        Vector3 base_scale = new Vector3(8, 1, 8);    //颜色和大小有关
        float rand_y = Random.Range(-0.5f, 0.5f);   //垂直方向速度随机生成
        int side = Random.Range(0, 2);               //水平速度方向
        if (side == 0)
        {
            side = -1;
        }
        int rand_num = Random.Range(1, 4);         //颜色和速度有关
        int[] point_str = { 1, 2, 3 };
        float[] speed_str = { 0.6f, 1.2f, 1.8f };
        float[] localScale_str = { 1.2f, 1f, 0.8f };

        disk.GetComponent<DiskData>().points = point_str[rand_num - 1];
        disk.GetComponent<DiskData>().speed = speed_str[rand_num - 1] * base_speed;
        disk.GetComponent<Transform>().localScale = localScale_str[rand_num - 1] * base_scale;
        disk.GetComponent<DiskData>().direction = new Vector3(side, rand_y, 0);
        if (rand_num == 1)
            disk.GetComponent<Renderer>().material.color = Color.red;
        else if (rand_num == 2)
            disk.GetComponent<Renderer>().material.color = Color.black;
        else
            disk.GetComponent<Renderer>().material.color = Color.white;

        use.Add(disk.GetComponent<DiskData>());

        return disk;
    }

    //释放
    public void FreeDisk(GameObject disk)
    {
        foreach (DiskData d in use)
        {
            if (d.gameObject.GetInstanceID() == disk.GetInstanceID())
            {
                disk.SetActive(false);  //先灭活
                use.Remove(d);
                free.Add(d);            //重新放回到对象池中
                break;
            }
        }
    }

    //销毁所有对象
    public void Clear()
    {
        foreach (DiskData disk in free)
        {
            disk.gameObject.transform.position = new Vector3(0, -20, 0);
            Destroy(disk.gameObject);
        }
        free.Clear();
        foreach (DiskData disk in use)
        {
            //让FlyAction结束并回调,再销毁对象。
            disk.gameObject.transform.position = new Vector3(0, -20, 0);
            Destroy(disk.gameObject);
        }
        use.Clear();
    }
}

2.DiskData 飞碟数据

public class DiskData : MonoBehaviour
{
    public float speed;         //水平速度
    public int points;          //击中红碟1分,黑碟2分,白碟3分
    public Vector3 direction;   //初始方向
}



(三)辅助类
1.Singleton单例模板
与教材相同

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;
        }
    }
}

2.ScoreRecorder计分者

public class ScoreRecorder : System.Object{
    public int score;

    public void Record(GameObject disk){
        score += disk.GetComponent<DiskData>().points;
    }
    
    public void Reset(){
        score = 0;
    }
}



(四)FirstController主控制器
FirstController负责初始化对象,从飞碟工厂中获取飞碟、回合控制、处理玩家击中、飞碟出界事件。

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

public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
    public DiskFactory diskFactory;
    public IActionManager actionManager;
    public ScoreRecorder scoreRecorder;
    public UserGUI userGUI;
    int round;          //回合
    float curr_time;     //当前时间,用于update的累计计算
    int curr_disk_num;       //该回合飞碟数量

    void Start()
    {
        //添加导演
        SSDirector.GetInstance().CurrentScenceController = this;

        //添加脚本,初始化对象
        gameObject.AddComponent<DiskFactory>();
        gameObject.AddComponent<CCActionManager>();
        userGUI = gameObject.AddComponent<UserGUI>();
        diskFactory = Singleton<DiskFactory>.Instance;
        actionManager = Singleton<CCActionManager>.Instance;

        //加载其他资源
        LoadResources();
    }

    public void LoadResources()
    {
        scoreRecorder = new ScoreRecorder();
        scoreRecorder.Reset();
        round = 0;
        curr_time = 0;
        curr_disk_num = 0;
    }

    //发射飞碟
    public void SendDisk()
    {
        GameObject disk = diskFactory.GetDisk(round);
        float side = -disk.GetComponent<DiskData>().direction.x;        //与速度水平方向相反
        disk.transform.position = new Vector3(side * 15f, UnityEngine.Random.Range(0f, 8f), 0);
        disk.SetActive(true);         //激活

        actionManager.Fly(disk, disk.GetComponent<DiskData>().speed, disk.GetComponent<DiskData>().direction);//设置飞行动作
    }

    //击中飞碟
    public void Click(Vector3 position)
    {
        Camera camera = Camera.main;
        Ray ray = camera.ScreenPointToRay(position);
        RaycastHit[] Hit = Physics.RaycastAll(ray);

        for (int i = 0; i < Hit.Length; i++)
        {
            if (Hit[i].collider.gameObject.GetComponent<DiskData>() != null)
            {
                GameObject disk = Hit[i].collider.gameObject;
                disk.transform.position = new Vector3(0, -20, 0);
                diskFactory.FreeDisk(disk);
                scoreRecorder.Record(disk);
                userGUI.score = scoreRecorder.score;
            }
        }
    }

    //清除界面中的飞碟
    public void ClearDisk()
    {
        GameObject[] obj = FindObjectsOfType(typeof(GameObject)) as GameObject[]; //获取所有gameobject元素
        foreach (GameObject child in obj)
        {
            if (child.gameObject.name == "Disk")
            {
                child.gameObject.SetActive(false);
            }
        }
        diskFactory.Clear();        //彻底销毁
    }

    public void Reset()
    {
        round = 0;
        curr_time = 0;
        curr_disk_num = 0;
        userGUI.round = round;
        scoreRecorder.Reset();
        userGUI.score = scoreRecorder.score;
        ClearDisk();
    }

    void Update()
    {
        if (userGUI.status == 1)
        {
            if (round <= 10)    //游戏未结束
            {
                curr_time += Time.deltaTime;
                if (curr_time > 2)          //每2秒生成一次
                {
                    curr_time = 0;
                    //游戏初始化
                    if (round == 0)
                    {
                        round++;
                        userGUI.round = round;
                    }
                    int rand_disk = Random.Range(1, 5);     //随机发射1-4个飞碟
                    for (int i = 0; i < rand_disk; i++)
                    {
                        SendDisk();
                        curr_disk_num++;
                        if (curr_disk_num == 10)
                        {
                            break;
                        }
                    }
                    if (curr_disk_num == 10 && round <= 10)     //发射10个后更新轮次
                    {
                        curr_disk_num = 0;
                        round++;
                        userGUI.round = round;
                    }
                }
            }

            if (round == 11)     //结束游戏
            {
                userGUI.round = 10;
                curr_time += Time.deltaTime;
                if (curr_time > 5)
                {
                    userGUI.status = 2;
                }
            }
        }
    }
}

其余代码大多沿用魔鬼与牧师,具体见项目地址

游戏截图

start
playing
over
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,888评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,677评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,386评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,726评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,729评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,337评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,902评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,807评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,349评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,439评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,567评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,242评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,933评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,420评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,531评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,995评论 3 377
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,585评论 2 359

推荐阅读更多精彩内容