unity3d_打飞碟

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

游戏内容要求:

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

游戏的要求:

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

游戏规则

  • 游戏总共分为3轮,每轮玩家共有5次生命,每漏掉一个飞盘就扣除一次生命,反之鼠标每点中一次飞盘,即可得到相应的分数;
  • 逐轮增加难度,随着轮次的上升,飞碟的飞行速度会更快;
  • 每个trail的飞碟的色彩,大小;发射位置,速度,角度,每次发射飞碟数量不一;

  由于每次出现一个飞碟就要创建一个对象实例,飞碟的销毁有需要销毁实例,这很大程度上增加了游戏运行成本,所以使用工厂模式。飞碟工厂用于单独管理预制之飞碟的创建和回收,飞碟工厂先创建一堆不同颜色的飞碟,然后需要飞碟时就从工厂中随机取出一个可用的飞碟,当飞碟不被需要时则将其放入工厂中,省去了飞碟实例创建和销毁的代价。

游戏项目的UML图如下

在这里插入图片描述

游戏实现

DiskFactory

飞碟工厂类中包含了飞碟的产生与回收,还包含了根据回合调整飞碟的飞出速度的设定。其中,使用了list来实现飞碟的产生和销毁,先创建飞碟实例放在free队列中,然后当需要飞碟的时候就从free队列中取,把使用过的飞碟放到used队列中。

public class DiskFactory : MonoBehaviour
{
    public GameObject disk_prefab = null;                 //飞碟预制体
    private List<DiskData> used = new List<DiskData>();   //正在被使用的飞碟列表
    private List<DiskData> free = new List<DiskData>();   //空闲的飞碟列表

    public GameObject GetDisk(int round)
    {
        int choice = 0;
        int scope1 = 1, scope2 = 4, scope3 = 7;           //随机的范围
        float start_y = -10f;                             //刚实例化时的飞碟的竖直位置
        string tag;
        disk_prefab = null;

        //根据回合,随机选择要飞出的飞碟
        if (round == 1)
        {
            choice = Random.Range(0, scope1);
        }
        else if(round == 2)
        {
            choice = Random.Range(0, scope2);
        }
        else
        {
            choice = Random.Range(0, scope3);
        }
        //将要选择的飞碟的tag
        if(choice <= scope1)
        {
            tag = "disk1";
        }
        else if(choice <= scope2 && choice > scope1)
        {
            tag = "disk2";
        }
        else
        {
            tag = "disk3";
        }
        //寻找相同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)
        {
            if (tag == "disk1")
            {
                disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk1"), new Vector3(0, start_y, 0), Quaternion.identity);
            }
            else if (tag == "disk2")
            {
                disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk2"), new Vector3(0, start_y, 0), Quaternion.identity);
            }
            else
            {
                disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk3"), new Vector3(0, start_y, 0), Quaternion.identity);
            }
            //给新实例化的飞碟赋予其他属性
            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;
            }
        }
    }
}
DiskData

Diskdata,飞碟数据类用来给飞碟提供属性,点击到此飞碟能得到的分数,飞碟的颜色,飞碟飞出的速度与方向。

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

public class DiskData : MonoBehaviour
{
    //射击此飞碟得分
    public int score = 1;
    //颜色
    public Color color = Color.red;
    //速度
    public float speed = 20;
    //方向
    public Vector3 direction;
}
RoundController

RoundController,场景控制器,场景控制器负责统筹管理游戏项目的每个组件,游戏一共分为三种状态:游戏开始,游戏进行中,游戏结束。根据游戏的情况,调用与飞碟共产的接口以使飞碟游戏的难度跟游戏轮次相匹配。场景控制器海涌到了定时器,相隔一定时间之后就发送飞碟,每当玩家漏点飞碟时更新相应的生命值。

public class RoundController : MonoBehaviour, ISceneController, IUserAction
{
    public DiskFactory diskFactory;
    public CCActionManager actionManager;
    public ScoreRecorder scoreRecorder;
    public UserGUI userGui;

    private Queue<GameObject> diskQueue = new Queue<GameObject>();
    private List<GameObject> diskMissed = new List<GameObject>();

    private int totalRound = 3;
    private int trialNumPerRound = 10;
    private int currentRound = -1;
    private int currentTrial = -1;
    private float throwSpeed = 2f;
    private int gameState = 0; //-1:失败 0:初始状态 1:进行中 2:胜利
    private float throwInterval = 0;
    private int userBlood = 10;

    void Awake()
    {
        SSDirector director = SSDirector.GetInstance();
        director.CurrentSceneController = this;
        
        diskFactory = Singleton<DiskFactory>.Instance;
        userGui = gameObject.AddComponent<UserGUI>() as UserGUI;
        actionManager = gameObject.AddComponent<CCActionManager>() as CCActionManager;

        scoreRecorder = new ScoreRecorder();
    }

    public void LoadResource()
    {
        diskQueue.Enqueue(diskFactory.GetDisk(currentRound));
    }

    public void ThrowDisk(int count)
    {
        while(diskQueue.Count <= count)
        {
            LoadResource();
        }
        for(int i = 0; i < count; i++)
        {
                    float position_x = 16;
        GameObject disk = diskQueue.Dequeue();
        diskMissed.Add(disk);
        disk.SetActive(true);
        //设置飞碟位置
        float ran_y = Random.Range(-3f, 3f);
        float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
        disk.GetComponent<DiskData>().direction = new Vector3(ran_x, ran_y, 0);
        Vector3 position = new Vector3(-disk.GetComponent<DiskData>().direction.x * position_x, ran_y, 0);
        disk.transform.position = position;
        //设置飞碟初始所受的力和角度
        float power = Random.Range(10f, 15f);
        float angle = Random.Range(15f, 28f);
        actionManager.diskFly(disk, angle, power);
        }
    }

    void levelUp()
    {
        currentRound += 1;
        throwSpeed -= 0.5f;
        currentTrial = 1;
    }

    void Update()
    {
        if(gameState == 1)
        {
            if(userBlood <= 0 || (currentRound == totalRound && currentTrial == trialNumPerRound))
            {
                GameOver();
                return;
            }
            else
            {
                if (currentTrial > trialNumPerRound)
                {
                    levelUp();
                }
                if (throwInterval > throwSpeed)
                {
                    int throwCount = generateCount(currentRound);
                    ThrowDisk(throwCount);
                    throwInterval = 0;
                    currentTrial += 1;
                }
                else
                {
                    throwInterval += Time.deltaTime;
                }
            }
        }
        for (int i = 0; i < diskMissed.Count; i++)
        {
            GameObject temp = diskMissed[i];
            //飞碟飞出摄像机视野且未被打中
            if (temp.transform.position.y < -8 && temp.gameObject.activeSelf == true)
            {
                diskFactory.FreeDisk(diskMissed[i]);
                diskMissed.Remove(diskMissed[i]);
                userBlood -= 1;
            }
        }
    }

    public int generateCount(int currentRound)
    {
        if(currentRound == 1)
        {
            return 1;
        }
        else if(currentRound == 2)
        {
            return Random.Range(1, 2);
        }
        else
        {
            return Random.Range(1, 3);
        }
    }


    public void StartGame()
    {
        gameState = 1;
        currentRound = 1;
        currentTrial = 1;
        userBlood = 10;
        throwSpeed = 2f;
        throwInterval = 0;
}

    public void GameOver()
    {
        if(userBlood <= 0)
        {
            gameState = -1;//失败
        }
        else
        {
            gameState = 2;//胜利
        }
    }

    public void Restart()
    {
        scoreRecorder.Reset();
        StartGame();
    }

    public void Hit(Vector3 pos)
    {
        Ray ray = Camera.main.ScreenPointToRay(pos);
        RaycastHit[] hits;
        hits = Physics.RaycastAll(ray);
        bool notHit = false;
        foreach (RaycastHit hit in hits)
            //射线打中物体
            if (hit.collider.gameObject.GetComponent<DiskData>() != null)
            {
                //射中的物体要在没有打中的飞碟列表中
                for (int j = 0; j < diskMissed.Count; j++)
                {
                    if (hit.collider.gameObject.GetInstanceID() == diskMissed[j].gameObject.GetInstanceID())
                    {
                        notHit = true;
                    }
                }
                if (!notHit)
                {
                    return;
                }
                diskMissed.Remove(hit.collider.gameObject);
                //记录分数
                scoreRecorder.Record(hit.collider.gameObject);
                diskFactory.FreeDisk(hit.collider.gameObject);
                break;
            }
    }

    public int GetScore()
    {
        return scoreRecorder.GetScore();
    }

    public int GetCurrentRound()
    {
        return currentRound;
    }
    public int GetBlood()
    {
        return userBlood;
    }
    public int GetGameState()
    {
        return gameState;
    }
}
游戏界面

游戏界面用来更新游戏和玩家的数据,包括得分数,生命值,当前回合等等。

public class UserGUI : MonoBehaviour
{
    private IUserAction action;
    private string score, round;
    int blood, gameState, HighestScore;

    // Start is called before the first frame update
    void Start()
    {
        action = SSDirector.GetInstance().CurrentSceneController as IUserAction;
    }

    // Update is called once per frame
    void Update()
    {
        gameState = action.GetGameState();
    }

    void OnGUI()
    {
        GUIStyle text_style;
        GUIStyle button_style;
        text_style = new GUIStyle()
        {
            fontSize = 20
        };
        button_style = new GUIStyle("button")
        {
            fontSize = 15
        };

        if (gameState == 0)
        {
            //初始界面
            if (GUI.Button(new Rect(Screen.width / 2 - 50, 80, 100, 60), "Start Game", button_style))
            {
                action.StartGame();
            }
        }
        else if(gameState == 1)
        {
            //游戏进行中
            //用户射击
            if (Input.GetButtonDown("Fire1"))
            {
                Vector3 mousePos = Input.mousePosition;
                action.Hit(mousePos);
            }

            score = "Score: " + action.GetScore().ToString();
            GUI.Label(new Rect(200, 5, 100, 100), score, text_style);
            round = "Round: " + action.GetCurrentRound().ToString();
            GUI.Label(new Rect(400, 5, 100, 100), round, text_style);

            blood = action.GetBlood();
            string bloodStr = "Blood: " + blood.ToString();
            GUI.Label(new Rect(600, 5, 50, 50), bloodStr, text_style);
        }
        else
        {
            //游戏结束,有两种情况
            if (gameState == 2)
            {

                if (action.GetScore() > HighestScore) {
                    HighestScore = action.GetScore();
                }       
                GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 250, 100, 60), "Game Over", text_style);
                string record = "Highest Score: " + HighestScore.ToString();  
                GUI.Label(new Rect(Screen.width / 2 - 70, Screen.height / 2 - 150, 150, 60), record, text_style);
            }
            else
            {
                GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 150, 100, 70), "You Lost!", text_style);
            }

            if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 30, 100, 60), "Restart", button_style))
            {
                action.Restart();
            }
        }
    }     
}
FlyAction

飞碟的飞行动作类,主要负责实现飞碟的飞行效果,包括给飞碟一个方向和力,让飞碟做带有类似重力加速的运动,当飞碟飞出游戏界面时将停止运动。

public class FlyAction : SSAction
{
    public float gravity = -5;                                 //向下的加速度
    private Vector3 start_vector;                              //初速度向量
    private Vector3 gravity_vector = Vector3.zero;             //加速度的向量,初始时为0
    private float time;                                        //已经过去的时间
    private Vector3 current_angle = Vector3.zero;               //当前时间的欧拉角

    private UFOFlyAction() { }
    public static UFOFlyAction GetSSAction(Vector3 direction, float angle, float power)
    {
        //初始化物体将要运动的初速度向量
        UFOFlyAction action = CreateInstance<UFOFlyAction>();
        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()
    {
        //计算物体的向下的速度,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坐标小于-10,动作就做完了
        if (this.transform.position.y < -10)
        {
            this.destroy = true;
            this.callback.SSActionEvent(this);      
        }
    }

    public override void Start() { }
}

效果展示

开始界面
在这里插入图片描述

游戏进行中的界面在这里插入图片描述
游戏结束界面在这里插入图片描述

由于老师临时将第五,六章节的作业合并,所以接下来对已经做好的Hit UFO进行完善

本次游戏的完善主要是引入Adapter模式,目的是想在原来设计的基础上让游戏增加一个物理运动模式,但是又不想删除之前所做好的运动模式,即不想放弃CCActionManager,所以我们新建PhysisActionManager类。

新的项目类图:

在这里插入图片描述
相比于上面的UML类图,这个UML类图多了一个PhysisActionManager类,并且我们需要设计一个统一的抽象接口,这是为了能够使用我们已经创建的两种运动方式,由此我们可以对运动模式进行选择。

Adapter(适配器)模式

目的:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

主要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。

项目实现

PhysisAction

这个类用于实现动力学运动模式,使得飞碟做自由落体运动

public class PhysisFlyAction : SSAction
{
    private Vector3 startVector;
    public float power;

    public static PhysisFlyAction GetSSAction(Vector3 direction, float angle, float power)
    {
        PhysisFlyAction action = CreateInstance<PhysisFlyAction>();
        if (direction.x == -1)
        {
            action.startVector = Quaternion.Euler(new Vector3(0, 1, -angle)) * Vector3.left * power;
        }
        else
        {
            action.startVector = Quaternion.Euler(new Vector3(0, 1, angle)) * Vector3.right * power;
        }
        action.power = power;
        return action;
    }

    public override void Start()
    {
        gameObject.GetComponent<Rigidbody>().velocity = power / 15 * startVector;
        gameObject.GetComponent<Rigidbody>().useGravity = true;
    }

    public override void Update()
    {
        if (this.transform.position.y < -10)
        {
            this.destory = true;
            this.callback.SSActionEvent(this);
        }
    }
}
PhysisActionManager

对飞碟的物理运动进行管理

public class PhysisActionManager : SSActionManager, ISSActionCallback
{
    public PhysisFlyAction fly;

    protected new void Start(){ }

    //飞碟飞行
    public void playDisk(GameObject disk, float angle, float power)
    {
        fly = PhysisFlyAction.GetSSAction(disk.GetComponent<DiskData>().direction, angle, power);
        this.RunAction(disk, fly, this);
    }

    #region ISSActionCallback implementation
    public void SSActionEvent(SSAction source,
        SSActionEventType events = SSActionEventType.Compeleted,
        int intParam = 0,
        string strParam = null,
        Object objectParam = null)
    {
        //回调函数,动作执行完后调用
    }
    #endregion
}
ActionManagerAdapter

实现完飞碟的物理运动之后,我们需要实现一个运动管理适配器来对不同的运动模式进行管理和调度。

public class ActionManagerAdapter : MonoBehaviour, IActionManager
{
    public CCActionManager CCAction;
    public PhysisActionManager PhysisAction;

    public void playDisk(GameObject disk, float angle, float power, bool isPhysis)
    {
        if (!isPhysis)
        {
            CCAction.playDisk(disk, angle, power);
        }
        else
        {
            PhysisAction.playDisk(disk, angle, power);
        }
    }

    void Start()
    {
        CCAction = gameObject.AddComponent<CCActionManager>() as CCActionManager;
        PhysisAction = gameObject.AddComponent<PhysisActionManager>() as PhysisActionManager;
    }
}
当然,我们还需要修改场景控制器

使得在游戏过程中可以随时切换运动管理器


public bool isPhysis = false;

//将场景的运动管理器由原先的 CCActionManager 改为 IActionManager
actionManager = gameObject.AddComponent<ActionManagerAdapter>() as IActionManager;

//为原先的函数调用增加一个参数
actionManager.playDisk(disk, angle, power, isPhysis);

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值