作业五

打飞碟

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

要求

  • 游戏有 n 个 round,每个 round 都包括10 次 trial
  1. 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
  2. 每个 trial 的飞碟有随机性,总体难度随 round 上升;
  3. 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
  • 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!
  • 近可能使用前面 MVC 结构实现人机交互与游戏模型分离

代码结构

还是采用MVC结构,具体可参考牧师与魔鬼的制作,本次主要是利用工厂模式完成不同飞碟的生产和回收。
在这里插入图片描述

代码说明

本次的核心代码主要是Disk和DiskFactory两部分,利用工厂模式来控制物体的产生和复用
对于Disk的实现,我们主要是定义其的属性以及相对应的get和set方法:

public class Disk : MonoBehaviour
{
    public Vector3 StartPoint
    {
        get
        {
            return gameObject.transform.position;
        }
        set
        {
            gameObject.transform.position = value;
        }
    }

    public Vector3 Direction
    {
        get
        {
            return Direction;
        }
        set
        {
            gameObject.transform.Rotate(value);
        }
    }

    public Color color
    {
        get
        {
            return gameObject.GetComponent<Renderer>().material.color;
        }
        set
        {
            gameObject.GetComponent<Renderer>().material.color = value;
        }
    }

    public float speed { get; set; }
}

对于DiskFactory,也就是Disk的制造工厂,屏蔽了生产和销毁的业务逻辑,只讲使用的结构提供给用户,使程序易于扩展。在代码中,我们还让DiskFactory负责在生产Disk时随机指定起始位置,方向,速度,颜色

public class DiskFactory
{
    public GameObject diskPrefab;
    public static DiskFactory DF = new DiskFactory();

    private Dictionary<int, Disk> used = new Dictionary<int, Disk>();
    private List<Disk> free = new List<Disk>();

    private DiskFactory()
    {
        diskPrefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Disk"));
        diskPrefab.AddComponent<Disk>();
        diskPrefab.SetActive(false);
    }

    public void FreeDisk()
    {
        foreach (Disk x in used.Values)
        {
            if (x.gameObject.activeSelf == false)
            {
                free.Add(x);
                used.Remove(x.GetInstanceID());
                return;
            }
        }
    }

    public Disk GetDisk(int round)
    {

        FreeDisk();
        GameObject newObject = null;
        Disk newDisk;
        if (free.Count > 0)
        {
            newObject = free[0].gameObject;
            free.Remove(free[0]);
        }
        else
            newObject = GameObject.Instantiate<GameObject>(diskPrefab, Vector3.zero, Quaternion.identity);
        newObject.SetActive(true);
        newDisk = newObject.AddComponent<Disk>();

        int swith;


        float s;
        if (round == 1)
        {
            swith = Random.Range(0, 3);
            s = Random.Range(30, 40);
        }
        else if (round == 2)
        {
            swith = Random.Range(0, 4);
            s = Random.Range(40, 50);
        }
        else
        {
            swith = Random.Range(0, 6);
            s = Random.Range(50, 60);
        }

        float PointX = UnityEngine.Random.Range(-1f, 1f) < 0 ? -1 : 1;
        newDisk.Direction = new Vector3(PointX, 1, 0);
        switch (swith)
        {
            case 0:
                newDisk.color = Color.yellow;
                newDisk.speed = s;
                newDisk.StartPoint = new Vector3(Random.Range(-130, -110), Random.Range(30, 90), Random.Range(110, 140));
                break;
            case 1:
                // ...
            case 2:
                // ...
            case 3:
                // ...
            case 4:
                // ...
            case 5:
                // ...
        }
        used.Add(newDisk.GetInstanceID(), newDisk);
        newDisk.name = newDisk.GetInstanceID().ToString();
        return newDisk;
    }
}

在运动分离部分,主要的修改就是重写CCMoveToAction,不同于牧师和魔鬼的上船下船,这里的运动更加简单,只需要让Disk做类似有水平初速自由落体的运动即可。

public class CCMoveToAction : SSAction
{
    public float speedx;
    public float speedy = 0;

    private CCMoveToAction() {}

    public static CCMoveToAction getAction(float speedx)
    {
        CCMoveToAction action = CreateInstance<CCMoveToAction>();
        action.speedx = speedx;
        return action;
    }

    public override void Start() {}

    public override void Update()
    {
        this.transform.position += new Vector3(speedx * Time.deltaTime, -speedy * Time.deltaTime + (float)-0.5 * 10 * Time.deltaTime * Time.deltaTime, 0);
        speedy += 10 * Time.deltaTime;
        if (transform.position.z == -1)
        {
            Debug.Log("Hit");
            destroy = true;
            CallBack.SSActionCallback(this, true);
        }
        else if (transform.position.y <= -45)
        {
            Debug.Log("Missing");
            destroy = true;
            CallBack.SSActionCallback(this, false);
        }
    }
}

运动管理类CCActionManager:

public class CCActionManager : SSActionManager, SSActionCallback
{
    int count = 0;
    public SSActionEventType Complete = SSActionEventType.Completed;
    UserAction UserActionController;

    public void MoveDisk(Disk Disk)
    {
        count ++;
        Complete = SSActionEventType.Started;
        CCMoveToAction action = CCMoveToAction.getAction(Disk.speed);
        addAction(Disk.gameObject, action, this);
    }

    public void SSActionCallback(SSAction source, bool isHit)
    {
        count --;
        Complete = SSActionEventType.Completed;
        UserActionController = SSDirector.getInstance().currentScenceController as UserAction;
        if (!isHit)
        {
            UserActionController.ReduceHealth();
        }
        source.gameObject.SetActive(false);
    }

    public bool IsAllFinished()
    {
        Debug.Log("isALLFInished");
        if (count == 0)
            return true;
        else return false;
    }
}

在SSAction类中,还是和牧师与魔鬼差不多的写法:

public class SSAction : ScriptableObject
{
    public bool enable = true;
    public bool destroy = false;

    public GameObject gameObject;
    public Transform transform;
    public SSActionCallback CallBack;

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

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

在SSActionManager类中,我们需要注意的是,一次性可能会有很多个飞碟在一起移动,所以我们实现了三个数据结构,一个等待的队列让每个新飞碟动作等待被调用,一个字典让每个飞碟运动的id和对应的action进行对应,每次进行遍历来调用其中的update函数实现飞碟的运动。最后是等待被删除的队列,当飞碟被击中或者移动出边界我们就让这个飞碟入队,等待执行销毁。至于判断游戏结束以及游戏结束之后的处理,我们下面再讲述。

public class SSActionManager : MonoBehaviour
{
    private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
    private List<SSAction> waitingToAdd = new List<SSAction>();
    private List<int> watingToDelete = new List<int>();
    UserAction UserActionController;

    private void Start()
    {
        UserActionController = SSDirector.getInstance().currentScenceController as UserAction;
    }

    protected void Update()
    {
        if (UserActionController.GetHealth() <= 0)
        {
            foreach (KeyValuePair<int, SSAction> kv in actions)
            {
                SSAction ac = kv.Value;
                ac.gameObject.transform.position = new Vector3(0, -5, 0);
                watingToDelete.Add(ac.GetInstanceID());
            }
            foreach (int key in watingToDelete)
            {
                SSAction ac = actions[key];
                actions.Remove(key);
                Object.Destroy(ac);
            }
            watingToDelete.Clear();
            return;
        }

        foreach (SSAction ac in waitingToAdd)
        {
            actions[ac.GetInstanceID()] = ac;
        }
        waitingToAdd.Clear();

        foreach (KeyValuePair<int, SSAction> kv in actions)
        {
            SSAction ac = kv.Value;
            if (ac.destroy)
            {
                watingToDelete.Add(ac.GetInstanceID());
            }
            else if (ac.enable)
            {
                ac.Update();
            }
        }

        foreach (int key in watingToDelete)
        {
            SSAction ac = actions[key];
            actions.Remove(key);
            Object.Destroy(ac);
        }
        watingToDelete.Clear();
    }

    public void addAction(GameObject gameObject, SSAction action, SSActionCallback ICallBack)
    {
        action.gameObject = gameObject;
        action.transform = gameObject.transform;
        action.CallBack = ICallBack;
        waitingToAdd.Add(action);
        action.Start();
    }
}

接口空间如下所示:

namespace Interfaces
{
    public interface ISceneController
    {
        void LoadResources();
    }

    public interface UserAction
    {
        void Hit(Vector3 pos);
        int GetScore();
        int GetRound();
        int GetHealth();
        void ReduceHealth();
        void GameOver();
        bool RoundStop();
        void Restart();
    }

    public enum SSActionEventType : int { Started, Completed }

    public interface SSActionCallback
    {
        void SSActionCallback(SSAction source, bool isHit);
    }
}

最后我们重写场记,导演和之前是一样的,可以参考牧师与魔鬼的导演,这里就不再列出。

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

public class FirstSceneController : MonoBehaviour, ISceneController, UserAction
{
    int score = 0;
    int round = 1;
    int tral = 0;
    int health = 5;
    bool start = false;
    bool gameOver = false;
    CCActionManager Manager;
    DiskFactory DF;

    void Awake()
    {
        SSDirector director = SSDirector.getInstance();
        director.currentScenceController = this;
        DF = DiskFactory.DF;
        Manager = GetComponent<CCActionManager>();
    }

    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    int count = 0;
    void Update()
    {
        if (health <= 0)
            gameOver = true;
        if (gameOver)
            return;
        if (start == true)
        {
            count ++;
            if (count >= 80)
            {
                count = 0;

                if (DF == null)
                {
                    Debug.LogWarning("DF is NUll!");
                    return;
                }
                tral ++;
                Disk d = DF.GetDisk(round);
                Manager.MoveDisk(d);
                if (tral == 10)
                {
                    round ++;
                    tral = 0;
                }
            }
        }
    }

    public void LoadResources()
    {

    }

    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)
            {
                Color c = hit.collider.gameObject.GetComponent<Renderer>().material.color;
                if (c == Color.yellow)
                    score += 1;
                if (c == Color.red)
                    score += 2;
                if (c == Color.black)
                    score += 3;
                GameObject explosion = Instantiate(Resources.Load<GameObject>("Prefabs/ParticleSystem"), hit.collider.gameObject.transform.position, Quaternion.identity);
                explosion.GetComponent<ParticleSystem>().Play();
                Object.Destroy(explosion, 0.1f);
                hit.collider.gameObject.transform.position = new Vector3(0, -400, -1);
            }
        }
    }

    public int GetScore()
    {
        return score;
    }

    public int GetRound()
    {
        return round;
    }

    public int GetHealth()
    {
        return health;
    }

    public void ReduceHealth()
    {
        health -= 1;
        if (health < 0)
            health = 0;
    }

    public void GameOver()
    {
        gameOver = true;
    }

    public bool RoundStop()
    {
        if (round > 3 || health <= 0)
        {
            start = false;
            return Manager.IsAllFinished();
        }
        else
            return false;
    }

    public void Restart()
    {
        score = 0;
        round = 1;
        health = 5;
        start = true;
        gameOver = false;
    }
}

还有一个比较细节的部分是,当生命值为0了之后,此时屏幕上可能还会有正在移动的飞碟,如果我们不对它们进行处理,可能会影响到下一句的游戏,这不是我们所希望的。所以我们判断到游戏结束之后,要在SSActionManager类中将所有飞碟移出屏幕并且清空其动作。再次重新开始游戏的时候就不会影响到下一局的游戏了。
在界面交互部分。可以根据自己的喜爱进行调整:

public class InterfaceGUI : MonoBehaviour
{
    UserAction UserActionController;
    public GameObject t;
    bool isStart = false;
    bool gameOver = false;
    float S;
    float Now;
    int round = 1;

    // Use this for initialization
    void Start()
    {
        UserActionController = SSDirector.getInstance().currentScenceController as UserAction;
        S = Time.time;
    }

    private void OnGUI()
    {
        if (gameOver)
        {
            isStart = false;
            GUI.Label(new Rect(Screen.width / 2 - 30, Screen.height / 2 - 50, 100, 20), "GameOver!");
            if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 30, 100, 50), "Confirm"))
            {
                gameOver = false;
                return;
            }
            return;
        }

        if (!isStart)
            S = Time.time;
        GUI.Label(new Rect(10, 10, 200, 20), "Time:  " + ((int)(Time.time - S)).ToString());
        GUI.Label(new Rect(10, 30, 200, 20), "Round:  " + round);
        GUI.Label(new Rect(10, 50, 200, 20), "Score: " + UserActionController.GetScore().ToString());
        GUI.Label(new Rect(10, 70, 200, 20), "Health:  " + UserActionController.GetHealth().ToString());

        if (!isStart && GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 30, 100, 50), "Start"))
        {
            S = Time.time;
            isStart = true;
            UserActionController.Restart();
        }

        if (isStart)
        {
            round = UserActionController.GetRound();
            if (Input.GetButtonDown("Fire1"))
            {
                Vector3 pos = Input.mousePosition;
                UserActionController.Hit(pos);
            }
            if (round > 3)
            {
                round = 3;
                if (UserActionController.RoundStop())
                {
                    isStart = false;
                }
            }
        }

        if (isStart && UserActionController.GetHealth() == 0)
        {
            gameOver = true;
        }
    }
}

实现效果

我们为了效果好看,也加入了天空盒。
在这里插入图片描述
我们还利用了粒子效果模拟打中飞碟之后的爆炸效果,这需要我们预制一个Particle System,在GameObject -> Effects里进行添加,然后设置粒子的数量以及容器的模样,并在代码中设置0.1s之后自动销毁:

GameObject explosion = Instantiate(Resources.Load<GameObject>("Prefabs/ParticleSystem"), hit.collider.gameObject.transform.position, Quaternion.identity);
explosion.GetComponent<ParticleSystem>().Play();
Object.Destroy(explosion, 0.1f);

最终的效果如下:
在这里插入图片描述

在这里插入图片描述

编写一个简单的自定义组件

用自定义组件定义几种飞碟,做成预制:
第一种比较简单的方法是直接将属性设置成public,然后就可以在菜单中找到对应的,比如官方文档给的一个例子:

using UnityEngine;
using System.Collections;

// This is not an editor script.
public class MyPlayer : MonoBehaviour
{
    public int armor = 75;
    public int damage = 25;
    public GameObject gun;

    void Update()
    {
        // Update logic here...
    }
}

这样我们就能够直接在菜单中设置armor,damage,gun三种属性了:
在这里插入图片描述
这里也可以使用Editor的方式,比如将飞碟的颜色属性加入到组件里面去:

using UnityEngine;
using UnityEditor;
using System.Collections;

[CustomEditor(typeof(Disk))]
public class DiskEditor : Editor
{
    private Disk _target { get { return target as Disk; } }

    public override void OnInspectorGUI()
    {
        _target.color = EditorGUILayout.ColorField(new GUIContent("ColorValue"), _target.color);
    }
}

一个比较全面的属性设置如下所示。
我们定义的变量类型可以如下:

public int intValue;
public float floatValue;
public string stringValue;
public bool boolValue;
public Vector3  vector3Value; 
public Course enumValue = Course.Chinese;
public Color colorValue = Color.white;
public Texture textureValue;

在Editor文件中,各种类型的变量添加组件的方式:

_target.intValue = EditorGUILayout.IntField("IntValue", _target.intValue);
_target.floatValue = EditorGUILayout.FloatField("FloatValue", _target.floatValue);
_target.stringValue = EditorGUILayout.TextField("StringValue", _target.stringValue);
_target.boolValue = EditorGUILayout.Toggle("BoolValue", _target.boolValue);
_target.vector3Value = EditorGUILayout.Vector3Field("Vector3Value", _target.vector3Value);
_target.enumValue = (Course)EditorGUILayout.EnumPopup("EnumValue", (Course)_target.enumValue);
_target.colorValue = EditorGUILayout.ColorField(new GUIContent("ColorValue"), _target.colorValue);
_target.textureValue = (Texture)EditorGUILayout.ObjectField("TextureValue", _target.textureValue, typeof(Texture), true);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值