unity3d课后练习(五)

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

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

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

游戏规则说明:
游戏一共包括3个round,分数达到10分进入round2,分数达到20分进入round3;
round1、round2、round3的飞碟颜色分别为:黄色、绿色、红色;玩家摧毁(点中)它们的得分是:1、2、3;
round越高,飞碟的发射频率越高,出现的高度(范围内的随机值)越来越低,以增加反应的难度;
玩家一共有5滴血,每当一个飞碟被鼠标miss了掉入屏幕下方,玩家会掉一滴血,掉完了游戏结束;

游戏效果图:
在这里插入图片描述
下面介绍代码实现。
· DiskComponent
首先,用一个组件来表示飞碟的属性——分数和位置。之所以不像以往的作业“牧师与魔鬼”那样,单独写一个飞碟类,并添加属性和飞行动作,是因为我们要实现带缓存的工厂模式管理不同飞碟的生产与回收,所以最好创建与动作分开来管理。

public class DiskComponent : MonoBehaviour
{                                      
    public Vector3 direction;
    public int score = 1;
}

· FlyActionManager
管理飞碟的飞行动作。当场记FirstManager需要飞碟飞行的时候,调用动作管理类的方法,让飞碟飞行。

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 UFOFly(GameObject disk, float angle, float power)
    {
        fly = DiskFlyAction.GetSSAction(disk.GetComponent<DiskComponent>().direction, angle, power);
        this.RunAction(disk, fly, this);
    }
}

· DiskFlyAction
实现飞碟的飞行动作。给飞碟一个力和一个垂直方向的加速度,再增加一个发射的角度,具体实现飞碟的飞行。当飞碟飞出了摄像机的范围(在我的游戏场景中是y坐标小于-10),则摧毁该对象,以便实现回收。

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

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

· SSAction
协助实现动作对象(飞碟)的管理,包含动作对象的状态、transform组件和消息通知对象。

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

· Interface
接口类,使项目的逻辑结构更清晰。

public interface ISceneController
{
    void LoadResources();
}

public interface IUserAction
{
    void Hit(Vector3 pos);
    int GetScore();
    void GameOver();
    void ReStart();
    void BeginGame();
}
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);
}

· DiskFactory
飞碟工厂类,根据回合的不同实现随机发送不同的飞碟。维护两个list,一个used表示正在被使用的飞碟列表;一个free表示空闲的飞碟列表。在需要发送飞碟的时候,DiskFactory先检查free列表中有没有飞碟,如果有,取出该飞碟并发送;如果free列表中没有飞碟,则实例化一个具有DiskComponent组件的飞碟。used列表中的飞碟被摧毁或者飞出了摄像机范围,则将该对象移出used列表,加入free列表。这样,就实现了对飞碟生产和回收的管理。

public class DiskFactory : MonoBehaviour
{
    public GameObject disk_prefab = null;                 //飞碟预制
    private static DiskFactory _instance;
    private List<DiskComponent> used = new List<DiskComponent>();   
    private List<DiskComponent> free = new List<DiskComponent>();   

    public static DiskFactory getInstance()
    {
        if (_instance == null)
        {
            _instance = new DiskFactory();
        }
        return _instance;
    }

    public GameObject GetDisk(int round)
    {
        float start_y = 7;                           
        string tag;
        disk_prefab = null;

        if (round == 1)
        {
            tag = "disk1";
        }
        else if (round == 2)
        {
            tag = "disk2";
        }
        else
        {
            tag = "disk3";
        }
        
        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)
        {
            if (tag == "disk1")
            {
                disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk1"), new Vector3(0, start_y, 10), Quaternion.identity);
            }
            else if (tag == "disk2")
            {
                disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk2"), new Vector3(0, start_y, 10), Quaternion.identity);
            }
            else
            {
                disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk3"), new Vector3(0, start_y, 10), Quaternion.identity);
            }
        }
        used.Add(disk_prefab.GetComponent<DiskComponent>());
        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;
            }
        }
    }
}

· FirstController
主要的场景控制器。主要完成游戏场景的控制和玩家点击事件,以及游戏状态的管理。
游戏开始时,加载资源并维护一个计时器,每隔一定时间发送一个飞碟。发送飞碟的方式是发送坐标信息给FlyActionManager类,调用Fly函数;如果游戏进入下一轮,则发送的时间间隔缩短;游戏是否进入下一轮是由分数控制的。玩家点击了飞碟或飞碟飞出了范围,则回收飞碟。
游戏状态有开始、进行、结束三种,较为简单。初始状态,玩家点击“开始”按钮开始游戏,飞碟开始发送;当玩家的血量降为0,则游戏结束,显示分数和“重新开始”按钮。

public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
    public FlyActionManager action_manager;
    public DiskFactory disk_factory;
    public UserGUI user_gui;

    private Queue<GameObject> disk_queue = new Queue<GameObject>();          //飞碟队列
    private List<GameObject> disk_notshot = new List<GameObject>();          //没有被打中的飞碟队列
    private int round = 1;                                                   
    private float speed = 2f;                                                //发射时间间隔
    private bool playing_game = false;                                       
    private bool game_over = false;                                          
    private bool game_start = false;                                        
    private int score_round2 = 10;                                           
    private int score_round3 = 20;                                          

    void Start()
    {
        SSDirector director = SSDirector.GetInstance();
        director.CurrentScenceController = this;
        disk_factory = Singleton<DiskFactory>.Instance;
        action_manager = gameObject.AddComponent<FlyActionManager>() as FlyActionManager;
        user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;
    }

    void Update()
    {
        if (game_start)
        {
            if (game_over)
            {
                CancelInvoke("LoadResources");
            }
            if (!playing_game)
            {
                InvokeRepeating("LoadResources", 1f, speed);
                playing_game = true;
            }
            SendDisk();
            if (user_gui.score >= score_round2 && round == 1)
            {
                round = 2;
                speed = speed - 0.6f;
                CancelInvoke("LoadResources");
                playing_game = false;
            }
            else if (user_gui.score >= score_round3 && round == 2)
            {
                round = 3;
                speed = speed - 0.5f;
                CancelInvoke("LoadResources");
                playing_game = false;
            }
        }
    }

    public void LoadResources()
    {
        disk_queue.Enqueue(disk_factory.GetDisk(round));
    }

    private void SendDisk()
    {
        float position_x = 16;
        if (disk_queue.Count != 0)
        {
            GameObject disk = disk_queue.Dequeue();
            disk_notshot.Add(disk);
            disk.SetActive(true);
            float ran_y = 6;
            float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
            if (round == 2)
            {
                disk.GetComponent<DiskComponent>().score = 2;
                float diff = Random.Range(1, 5);
                ran_y -= diff;   
            }
            else if(round == 3)
            {
                disk.GetComponent<DiskComponent>().score = 3;
                float diff = Random.Range(6, 9);
                ran_y -= 6;
            }
            disk.GetComponent<DiskComponent>().direction = new Vector3(ran_x, ran_y, 10);
            Vector3 position = new Vector3(-disk.GetComponent<DiskComponent>().direction.x * position_x, ran_y, 15);
            disk.transform.position = position;
            float power = Random.Range(10f, 15f);
            float angle = Random.Range(0, 0);
            action_manager.UFOFly(disk, angle, power);
        }

        for (int i = 0; i < disk_notshot.Count; i++)
        {
            GameObject temp = disk_notshot[i];
            if (temp.transform.position.y < -10 && temp.gameObject.activeSelf == true)
            {
                disk_factory.FreeDisk(disk_notshot[i]);
                disk_notshot.Remove(disk_notshot[i]);
                user_gui.ReduceBlood();
            }
        }
    }

    public void Hit(Vector3 pos)
    {
        Ray ray = Camera.main.ScreenPointToRay(pos);
        RaycastHit[] hits;
        hits = Physics.RaycastAll(ray);
        bool not_hit = false;
        for (int i = 0; i < hits.Length; i++)
        {
            RaycastHit hit = hits[i];
            if (hit.collider.gameObject.GetComponent<DiskComponent>() != null)
            {
                for (int j = 0; j < disk_notshot.Count; j++)
                {
                    if (hit.collider.gameObject.GetInstanceID() == disk_notshot[j].gameObject.GetInstanceID())
                    {
                        not_hit = true;
                    }
                }
                if (!not_hit)
                {
                    return;
                }
                disk_notshot.Remove(hit.collider.gameObject);
                user_gui.Record(hit.collider.gameObject);
                //回收飞碟
                StartCoroutine(WaitingParticle(0.08f, hit, disk_factory, hit.collider.gameObject));
                break;
            }
        }
    }

    public int GetScore()
    {
        return user_gui.score;
    }

    public void ReStart()
    {
        game_over = false;
        playing_game = false;
        user_gui.score = 0;
        round = 1;
        speed = 2f;
    }

    public void GameOver()
    {
        game_over = true;
    }

    IEnumerator WaitingParticle(float wait_time, RaycastHit hit, DiskFactory disk_factory, GameObject obj)
    {
        yield return new WaitForSeconds(wait_time);
        hit.collider.gameObject.transform.position = new Vector3(0, -9, 0);
        disk_factory.FreeDisk(obj);
    }
    public void BeginGame()
    {
        game_start = true;
    }
}

· UserGUI
最后是用户交互界面,主要显示分数、玩家血量和控制按钮等。由于用户的血量和分数控制都较为简单,并且相对独立,我就没有把它们单独写成一个类,而是直接在本类中进行控制。这样做从某种意义上来说是不符合MVC模式的,因为没有实现控制器和视图的分离。但是,考虑到控制器只有短短几行代码,仅仅将一个函数封装成类,难免显得过度封装、代码不简洁。所以,考虑之后我还是没有将它们分离出来。

public class UserGUI : MonoBehaviour
{
    private IUserAction action;
    public int life = 5;  
    public int score = 0;  

    GUIStyle bold_style = new GUIStyle();
    GUIStyle score_style = new GUIStyle();
    GUIStyle text_style = new GUIStyle();
    GUIStyle over_style = new GUIStyle();
    private int high_score = 0;            
    private bool game_start = false;       

    void Start()
    {
        action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
    }

    public void Record(GameObject disk)
    {
        int temp = disk.GetComponent<DiskComponent>().score;
        score = temp + score;
    }

    void OnGUI()
    {
        bold_style.normal.textColor = new Color(1, 0, 0);
        bold_style.fontSize = 20;
        text_style.normal.textColor = new Color(1, 1, 1);
        text_style.fontSize = 20;
        score_style.normal.textColor = new Color(0, 1, 0);
        score_style.fontSize = 20;
        over_style.normal.textColor = new Color(1, 0, 0);
        over_style.fontSize = 25;

        if (game_start)
        {
            //射击
            if (Input.GetButtonDown("Fire1"))
            {
                Vector3 pos = Input.mousePosition;
                action.Hit(pos);
            }

            GUI.Label(new Rect(450, 400, 200, 50), "分数:", text_style);
            GUI.Label(new Rect(500, 400, 200, 50), action.GetScore().ToString(), score_style);
            GUI.Label(new Rect(Screen.width - 500, 400, 50, 50), "生命:", text_style);
            for (int i = 0; i < life; i++)
            {
                GUI.Label(new Rect(Screen.width - 450 + 10 * i, 400, 50, 50), "0", bold_style);
            }

            //游戏结束
            if (life == 0)
            {
                high_score = high_score > action.GetScore() ? high_score : action.GetScore();
                GUI.Label(new Rect(Screen.width / 2 - 20, Screen.width / 2 - 250, 100, 100), "游戏结束", over_style);
                GUI.Label(new Rect(Screen.width / 2 - 10, Screen.width / 2 - 200, 50, 50), "最高分:", text_style);
                GUI.Label(new Rect(Screen.width / 2 + 50, Screen.width / 2 - 200, 50, 50), high_score.ToString(), text_style);
                if (GUI.Button(new Rect(Screen.width / 2 - 20, Screen.width / 2 - 320, 100, 50), "重新开始"))
                {
                    life = 6;
                    action.ReStart();
                    return;
                }
                action.GameOver();
            }
        }
        else
        {
            if (GUI.Button(new Rect(Screen.width / 2 - 20, Screen.width / 2 - 300, 100, 50), "开始"))
            {
                game_start = true;
                action.BeginGame();
            }
        }
    }
    public void ReduceBlood()
    {
        if (life > 0)
            life--;
    }
}

做完这些,再添加一个Skybox来美化一下游戏场景,就大功告成啦。
GitHub传送门:Disk
鸣谢师兄提供的博客供我参考!


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

用自定义组件定义几种飞碟,做成预制。参考官方脚本手册 https://docs.unity3d.com/ScriptReference/Editor.html
实现自定义组件,编辑并赋予飞碟一些属性。
写好上文描述的DiskComponent后,我们需要将该脚本挂载在飞碟预设上。之后,新建一个DiskEditor类,先序列化属性,然后可以修改属性并应用。
自定义组件可以大大提高编程效率,使我们避免重复劳动。

[CustomEditor(typeof(DiskData))]
[CanEditMultipleObjects]
public class DiskEditor : Editor
{
	//序列化飞碟的属性,如DiskComponent中的score
	//要丰富自定义组件的内容,可以给飞碟增加一些额外的属性,比如大小、颜色等等
	SerializedProperty score;  

	void OnEnable()
	{
		score = serializedObject.FindProperty("score");
	}

	public override void OnInspectorGUI()
	{
		serializedObject.Update();
		EditorGUILayout.IntSlider(score, 0, 5, new GUIContent("score"));
		//应用修改
		serializedObject.ApplyModifiedProperties();
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值