Unity3D游戏编程-鼠标打飞碟

Unity3D游戏编程-鼠标打飞碟

一、作业要求

1、编写一个简单的鼠标打飞碟(Hit UFO)游戏。
游戏内容要求:

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

游戏的要求:

  1. 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类;
  2. 近可能使用前面 MVC 结构实现人机交互与游戏模型分离;
  3. 按 adapter模式 设计图修改飞碟游戏,使它同时支持物理运动与运动学(变换)运动。

二、项目配置

Windows 10
Unity 2020.3.17f1c1

三、项目演示

视频演示

点击此处可以前往
可开启字幕说明

项目下载

下载Assets文件夹
点击此处可以前往gitee

文字说明

  1. 创建unity专案后,将保存的文件夹中的Assets替换成在上面项目下载的Assets文件夹
  2. 打开专案,然后点选Assets中的play加载场景
  3. 运行即可开始游戏
  4. 点击左方的Start with Physics以物理学的方式开启游戏,点击右方Start with Kinematics以运动学的方式开启
  5. 游戏有三轮,第一轮仅有白色飞碟(1分),第二轮加入黄色飞碟(2分),第三轮加入蓝色飞碟(4分)
  6. 当总分超过30分,游戏胜利
  7. 游戏进行时可以随时暂停

项目截图

请添加图片描述
请添加图片描述
请添加图片描述


四、前置内容

MVC模式

在之前牧师与恶魔的游戏设计中,我们接触到了MVC结构,此处再复习一下:
请添加图片描述

  1. SSDirector负责控制场景,把握全局
  2. FirstController负责控制布景(实现SceneController的函数),管理动作执行(实现UserAction的函数)
  3. SceneController负责设置布景,其本质是一个接口
  4. UserAction负责游戏操作,本质是一个接口
  5. GUI负责游戏的画面交互

更详细的说明可以看之前的作业:
点击此处查看

动作管理器

并且在本次实现中,也有用到动作管理器:
请添加图片描述
关于动作管理器的相关作业可以点击此处查看

适配器

适配器将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
适配器的优点:
将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。适配器还增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。


五、实现过程和方法(算法)

运动学和物理学运动方式,主要的区别在于,物理学执行动作的时候不需要我们使用transform.Translate()。
并且通过前几次作业的结合,可以简单画出本次作业程序的结构:
请添加图片描述


Director

Director类和上次的编写一模一样,也是利用单例模式和懒汉模式。

SceneController

SceneController也是一个接口,但这一次的游戏并不需要预先加载角色(上次要加载牧师、恶魔、船之类的对象,但这次没有),所以loadResources在FirstController的实现为空。

public interface ISceneControl
{
    void loadResources();
}

UserAction

UserAction是门面模式,与用户交互相关的,与玩家动作有关:

public enum GameState { ROUND_START, ROUND_FINISH, RUNNING, PAUSE, START, FUNISH }

public interface IUserAction
{
    GameState getGameState();//获取游戏状态
    void setGameState(GameState gameState);//设置游戏状态
    int getScore();//获取当前积分
    void hit(Vector3 pos);//
    bool getActionMode();//获取模式状态(运动学还是动力学)
    void setActionMode(bool mode);//设置模式
}

FristSceneControl

FristSceneControl就要实现上面(SceneController、UserAction)两个类的函数。

  1. 先看一些初始设置
        public ActionManagerAdapter actionManager { set; get; }//适配器
        public ScoreRecorder scoreRecorder { set; get; }       //成绩记录器
        public Queue<GameObject> diskQueue = new Queue<GameObject>();//飞碟对象队列
        private int diskNumber = 0;//飞碟的数量,主要用于设定每一轮抛出的飞碟数量
        private int currentRound = -1;//当前进行到第几局
        private float time = 0;//时间控制,用来做抛飞碟的时候用到
        private GameState gameState = GameState.START;//游戏状态
        UserGUI userGUI;//挂载GUI
        private bool isPhysical = false;//用于判断是否使用物理引擎

· 这次作业需要实现让运动学和动力学两种模式的共存,因此就需要利用适配器。对于FirstSceneActionManager,有两个类,一个是CCActionManager,负责运动学的行为;另一个是ModifiedActionManager,负责物理学的行为。
· ScoreRecorder类是专门负责记录成绩的

  1. 对于项目的初始化(Awake):
        void Awake()
        {
            Director director = Director.getInstance();//获取导演
            director.current = this;
            diskNumber = 10;//初始化每轮抛出的飞碟数
            this.gameObject.AddComponent<ScoreRecorder>();//挂载ScoreRecorder
            this.gameObject.AddComponent<DiskFactory>();//挂载DiskFactory
            scoreRecorder = Singleton<ScoreRecorder>.Instance;
            userGUI = gameObject.AddComponent <UserGUI>() as UserGUI;//挂载UserGUI

            director.current.loadResources();
        }

· scoreRecorder = Singleton<ScoreRecorder>.Instance;使得我们获取ScoreRecorder的单例对象。Singleton类的写法就是老师给出的模板类。

  1. SceneController中的loadResources实现,本次为空。
public void loadResources()
        {
        }
  1. UserAction的getGameState(),目的是将变量gameState进行return
  2. UserAction的setGameState(GameState gameStateIn),设置变量gameStat,将gameStat设置为传入的参数即可
  3. UserAction的getScore(),返回变量scoreRecorder.score
  4. UserAction的hit(Vector3 pos)主要利用光标拾取多个物体的程序:
        public void hit(Vector3 pos)
        {
            RaycastHit[] hits = Physics.RaycastAll(Camera.main.ScreenPointToRay(pos));
            for (int i = 0; i < hits.Length; i++)
            {
                RaycastHit hit = hits[i];
                if (hit.collider.gameObject.GetComponent<DiskData>() != null)
                {
                    scoreRecorder.record(hit.collider.gameObject);
                    hit.collider.gameObject.transform.position = new Vector3(0, -5, 0);
                }
            }
        }

· RaycastHit是用于检测碰撞的
· Camera.main.ScreenPointToRay(pos)表示一个从摄像机原点出发指向目标位置pos的射线,Physics.RaycastAll得到碰撞结果存储在RaycastHit
· 如果击中的对象是有效飞碟,则计算分数以及把这个飞碟弄走

  1. UserAction的getActionMode(),返回变量isPhysical即可。
  2. UserAction的setActionMode(bool mode),设置变量isPhysical为传入参数。
  3. 之后是FirstControl对游戏场景的更新控制:
private void Update()
        {
            //因为需要依赖actionManager的功能
            // 注意actionManager是适配器,根据isPhysical的值采用不同的actionManager
            if (actionManager == null)
            {
                return;
            }

            if (actionManager.getDiskNumber() == 0 && gameState == GameState.RUNNING)//飞碟已经出完,并且游戏进行中,就表示round结束
            {
                gameState = GameState.ROUND_FINISH;
                if (currentRound == 2)//如果round = 2,表示已经完成了三局游戏,游戏结束
                {
                    gameState = GameState.FUNISH;
                    return;
                }
            }

            if (actionManager.getDiskNumber() == 0 && gameState == GameState.ROUND_START)//飞碟数为空,并且是round开始状态,则准备开始下一round游戏
            {
                currentRound++;//回合数增加
                nextRound();//下一回合的准备
                actionManager.setDiskNumber(10);//重置飞碟数目
                gameState = GameState.RUNNING;//改变游戏状态
            }

            if (time > 1 && gameState != GameState.PAUSE)//其他时候执行抛飞碟的动作
            {
                throwDisk();
                time = 0;
            }
            else
            {
                time += Time.deltaTime;//time主要用来控制抛飞碟的时间间隔
            }
        }

而其中nextRound():

        private void nextRound()
        {
            DiskFactory diskFactory = Singleton<DiskFactory>.Instance;//获取飞碟工厂单例
            for (int i = 0; i < diskNumber; i++)
            {
                diskQueue.Enqueue(diskFactory.getDisk(currentRound,isPhysical));//根据当前round数和是否使用物理引擎(启动刚体)来取出飞碟,并且让它加入本局抛的飞碟队列
            }
            actionManager.startThrow(diskQueue);//开始抛飞碟(执行动作)
        }

throwDisk()

        void throwDisk()
        {
            if (diskQueue.Count != 0)
            {
                GameObject disk = diskQueue.Dequeue();//从队列取出一个飞碟
                Vector3 pos = new Vector3(-disk.GetComponent<DiskData>().getDirection().x * 10, Random.Range(0f, 4f), 0);
                disk.transform.position = pos;//设置目标位置
                disk.SetActive(true);
            }
        }

nextRound中的startThrow是让飞碟队列的每个飞碟都用函数runAction执行了动作,但是实际上要等throwDisk中SetActive之后才会实际运动。


ActionManagerAdapter

因为FristSceneContoller要调用ActionManager的函数,并且这次由于有两种模式的存在,所以FristSceneContoller会通过ActionManagerAdapter(适配器)来调用相关模式下的函数。
适配器本质上就是一个接口,而继承适配器的类就要实现这些接口,也通过适配器进行了统一。

    public interface ActionManagerAdapter
    {
        void setDiskNumber(int dn);//设置飞碟数目
        int getDiskNumber();//给继承的类获得飞碟数目
        SSAction getSSAction();//设置动作方式
        void freeSSAction(SSAction action);//释放某个动作
        void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Completed, int intPram = 0, string strParm = null, Object objParm = null);
        //callback的函数,用于告诉类某个动作已经完成
        void startThrow(Queue<GameObject> diskQueue);//开始抛飞碟(执行动作)
    }

然后就是运动学CCActionManager和物理学ModifiedActionManager各自对这个函数进行实现。


CCActionManager与ModifiedActionManager

这两个类在这次的作业中其实代码差不多,主要区别是Update中一个要使用刚体一个不用。
对于CCActionManager:

    protected void Start()
    {
        sceneControl = (FirstSceneControl)Director.getInstance().current;
        sceneControl.actionManager = this;
        flys.Add(CCFlyAction.getCCFlyAction());
        base.flag = true;//这个flag如果是true表示使用运动学,false表示使用物理学
    }

    private new void Update()
    {
        if (sceneControl.getGameState() == GameState.RUNNING)
            base.Update();
    }

而ModifiedActionManager:

    protected void Start()
    {
        sceneControl = (FirstSceneControl)Director.getInstance().current;
        sceneControl.actionManager = this;
        flys.Add(CCFlyAction.getCCFlyAction());
        base.flag = false;//使用物理学
    }

    private new void Update()                                     
    {
        if (sceneControl.getGameState() == GameState.RUNNING)
        {
            base.Update();
            base.startRigidbodyAction();//启动刚体
        } 
        else
        {
            base.stopRigidbodyAction();//停止刚体
        }
    }

SSActionManager

ActionManager跟上次作业的基本相似,多了要判断哪一种运动方式用不同的update:

protected void Update()
    {
        //......

        foreach (KeyValuePair<int, SSAction> i in actions)
        {
            SSAction value = i.Value;
            if (value.destroy)
            {
                waitingDelete.Add(value.GetInstanceID());
            }
            else if (value.enable && flag)//flag就是对应上面说使用哪种运动方式的变量
            {
                value.Update();
            }
            else if(value.enable && !flag)
            {
                value.FixedUpdate();
            }
        }

        //......
    }

还有启动和停止刚体的函数:

    public void stopRigidbodyAction()
    {
        foreach (SSAction action in actions.Values)
        {
            action.rigidbodyStopAction();
        }
    }

    public void startRigidbodyAction()
    {
        foreach (SSAction action in actions.Values)
        {
            action.rigidbodyStartAction();
        }
    }

SSAction

SSAction也实际上是一个接口,在本次作业的程序中,类CCFlyAction就负责实现这个最底部被继承的动作类。


CCFlyAction

CCFlyAction是负责实现飞碟飞行动作的类,它负责设置了大部分的变量参数。

    float acceleration;//加速度
    float horizontalSpeed;//水平速度
    Vector3 direction;
    float time;
    bool flag=false;//记录游戏是否经过暂停状态
    Vector3 temp;//记录游戏暂停时刚体游戏对象的速度矢量
    Rigidbody rigidbody; //物理运动,添加刚体

    //变量的初始化
    public override void Start()
    {
        enable = true;
        acceleration = 9.8f;
        time = 0;
        horizontalSpeed = gameObject.GetComponent<DiskData>().getSpeed();
        direction = gameObject.GetComponent<DiskData>().getDirection();
        //执行行为的对象如果有刚体性质,则需要设置刚体的速度属性
        rigidbody = gameObject.GetComponent<Rigidbody>();
        if (rigidbody)
        {
            rigidbody.velocity = horizontalSpeed * direction;
            temp = rigidbody.velocity;
        }
    }

然后是行为的实际执行方式:

    public override void Update()
    {
        if (gameObject.activeSelf)
        {
            time += Time.deltaTime;
            transform.Translate(Vector3.down * acceleration * time * Time.deltaTime);
            transform.Translate(direction * horizontalSpeed * Time.deltaTime);
            //运用运动学的方式,需要我们自己手动使用transform.Translate函数并且给出计算公式
            if (this.transform.position.y < -4)
            {
                this.destroy = true;
                this.enable = false;
                this.callback.SSActionEvent(this);
            }
        }
    }

    public override void FixedUpdate()//物理学方式的运动
    {
        if (gameObject.activeSelf)
        {
            //对比运动学方式就没有了要自行使用transform.Translate的步骤
            if (this.transform.position.y < -4)
            {
                this.destroy = true;
                this.enable = false;
                this.callback.SSActionEvent(this);
            }
        }
    }

DiskFactory

DiskFactory给我感觉有点像线程池,预先创建了一些飞碟,当需要用的时候才会取出。

对于一个飞碟,有以下的一些参数:

    private Vector3 size;//大小
    private Color color;//颜色
    private float speed;//运动速度
    private Vector3 direction;//位置

而工厂应该要有一个空闲队列和使用队列,用来记录哪些飞碟目前还没被使用,哪些已经被Active:

    public List<DiskData> used = new List<DiskData>();
    public List<DiskData> free = new List<DiskData>();

工厂的Awake()函数是加载飞碟对象的预建,并且设置它非Active:

    private void Awake()
    {
        diskPrefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Disk"), Vector3.zero, Quaternion.identity);
        diskPrefab.SetActive(false);
    }

此时的飞碟的参数都并没有被设置,也就是大小颜色那些都没有设定。

之后是从工厂种取出飞碟的getDisk()函数,传入的参数是当前第几轮(用以判断可以出什么颜色的飞碟)、是否使用刚体。getDisk()的工作主要有:

  1. 判断空闲队列还有没有飞碟,有的话直接取出空闲队列中的一个飞碟,并且把它从空闲队列移除。
  2. 如果空闲队列没有飞碟,则重新创建一个飞碟对象直接使用。
  3. 判断是否利用刚体,如果是的话,给刚刚所取出的飞碟套上刚体属性。
  4. 根据当前所进行到的回合数(约往后给出的飞碟会变小并且速度更快),设定飞碟的大小、颜色、速度、位置。
  5. return这个飞碟对象

当一个飞碟使用完毕进行回收时,可以利用freeDisk(GameObject disk)函数。

  1. 要把它设置成非Active
  2. 把这个飞碟放到空闲队列,并且从使用队列中移除。

ScoreRecorder

ScoreRecorder就是拿来记录不同颜色的飞碟对应的分数,以及当玩家击中飞碟时计算总得分。
因此它的初始化是设置总分为0、设置各种颜色飞碟对应多少分:

        void Start()                                        /* 初始化各种颜色的分数值 */
        {
            score = 0;
            scoreTable.Add(Color.white, 1);
            scoreTable.Add(Color.gray, 2);
            scoreTable.Add(Color.black, 4);
        }

当击中某个飞碟时,就可以调用record(GameObject disk)函数,把飞碟对象传入,让这个函数去计算得分:

        public void record(GameObject disk)
        {
            score += scoreTable[disk.GetComponent<DiskData>().getColor()];
        }

六、参考资料

1.Unity3d-learning 物理碰撞打飞碟小游戏
2.Unity3D 入门小技巧——鼠标拾取并移动物体(示例代码)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值