欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

3D游戏编程与设计 HW 6 物理系统与碰撞

程序员文章站 2022-07-13 08:35:06
...

3D游戏编程与设计 HW 6物理系统与碰撞

Hit UFO v2.0 完整游戏过程可见以下视频:
https://www.bilibili.com/video/BV19p4y1r7MJ/

Hit UFO v2.0 完整代码可见以下仓库:
https://gitee.com/beilineili/game3-d

1.改进飞碟(Hit UFO v2.0)游戏:

①游戏内容要求

  • 按 adapter模式 设计图修改飞碟游戏
  • 使它同时支持物理运动与运动学(变换)运动

2.游戏设计

① 游戏玩法

  • 通过用鼠标点击飞出来的UFO来得分,其中绿色飞碟为1分,黄色飞碟为2分,红色飞碟为3分
  • 分为四个游戏模式,分为简单,普通,困难和无限模式,其中开始时游戏默认是 Normal 模式。
  • 除了无限模式有无限轮次的飞碟外,其他模式都是有10轮,所有飞碟发射完毕时游戏结束

② 与 Hit UFO v1.0 版本的不同

Hit UFO v1.0(基础版)完整代码:
https://gitee.com/beilineili/game3-d/tree/master/4.1.Hit_UFO

  • 这次的改动建立在之前的 Hit UFO 基础上,在 Hit UFO v2.0 中,我们不删除原本的正常运动模式,而是让其与新的物理运动模式共存,在游戏运行的时候来决定使用哪种。即,原本的动作管理器类不删除,它们是管理正常运动模式的。我们再实现另一个用来管理物理运动模式的动作管理器。最后将它们结合起来
  • 物理引擎将游戏世界对象赋予现实世界的物理属性,并抽象为刚体模型,使得游戏物体在力的作用下,仿真现实世界的运动及其之间的碰撞过程。
  • 关于物理引擎的动作改动肯定离不开对动作类的更改,这里我们引入 Adapter 模式

  Adapter Pattern(适配器模式)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。
  这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。我们希望 FirstController 只需为同一个用途的所有组件保存1个变量,要将两个动作管理器同时接入 FirstController,就要实现一个适配器,让 FirstController 连接适配器,然后让适配器分别连接两个动作管理器。
  举个例子,当电源插头不够用时,我们会买一个插座来让更多的设备可以用上电。

③ 代码设计

一、Controller

1)修改 FlyAction.cs
  • 原先的 Start 函数是空实现,在这里因为 FlyAction 是运动学的飞行,所以生成对象时要将刚体设为运动学的,即不会碰撞检测
  • 将飞行运动拆分成水平和竖直两个方向,其中水平方向速度恒定,竖直方向添加重力加速度
  • 当飞碟到达底部时,即飞碟的高度在摄像机观察范围之下时,动作结束,将进行回调
public class FlyAction : SSAction
{
    float gravity;          //重力加速度
    float speed;            //初始速度
    float time;             //时间
    Vector3 direction;      //初始飞行方向

    public static FlyAction GetSSAction(Vector3 direction, float speed)
    {
        FlyAction action = ScriptableObject.CreateInstance<FlyAction>();
        action.gravity = 9.8f;
        action.time = 0;
        action.speed = speed;
        action.direction = direction;
        return action;
    }

    public override void Start()
    {
        gameObject.GetComponent<Rigidbody>().isKinematic = true;
    }

    public override void Update()
    {
        time += Time.deltaTime;
        //竖直方向上有重力加速度
        transform.Translate(Vector3.down * gravity * time * Time.deltaTime);
        //初始飞行方向匀速运动
        transform.Translate(direction * speed * Time.deltaTime);
        //飞碟到达画面底部,动作结束,进行回调
        if (this.transform.position.y < -6) 
        {
            this.destroy = true;
            this.enable = false;
            this.callback.SSActionEvent(this);
        }
        
    }
}

2)新增 IActionManager.cs
  • 动作管理类的接口,基于这个接口来实现动作管理类的 Adapter 模式
public interface IActionManager
{
      void Fly(GameObject disk, float speed, Vector3 direction);
}
3) 修改 ActionManager.cs
  • 飞行动作管理类,用于生成飞碟飞行动作,和接受飞行动作的回调信息,回收飞碟
  • ActionManager 现在需要实现 IActionManager 的接口
public class ActionManager : SSActionManager, ISSActionCallback, IActionManager
{
    //飞行动作
    public FlyAction flyAction;
    //控制器
    public FirstController controller;

    protected new void Start()
    {
        controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
        controller.actionManager = this;
    }

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

    //回调函数
    public void SSActionEvent(SSAction source,
                              SSActionEventType events = SSActionEventType.Competed,
                              int intParam = 0,
                              string strParam = null,
                              Object objectParam = null)
    {
        //飞碟结束飞行后工厂进行回收
        controller.diskFactory.FreeDisk(source.gameObject);
    }
}
4) 修改 IUserAction.cs
  • 用户动作接口,有点击,重新开始和选择游戏模式三个函数的接口
  • 现在 IUserAction 提供了设置飞行模式的接口
public interface IUserAction
{
    void Hit(Vector3 position); //点击
    void Restart(); //重新开始
    void SetMode(int mode); //选择游戏模式
    void SetFlyMode(int flyMode); //设置飞碟运动模式
}
5)修改 FirstController.cs
  • 场景控制器,负责游戏逻辑
  • 发送飞碟 SendDisk – 从工厂获得一个飞碟并设置初始位置和速度,飞行动作
  • 点击判断 Hit – 处理用户点击动作,将被点击到的飞碟回收,计算得分

在游戏中,玩家通过鼠标点击飞碟,从而得分。这当中涉及到一个点击判断的问题。我们调用 ScreenPointToRay 方法,构造由摄像头和屏幕点击点确定的射线,与射线碰撞的游戏对象即为玩家点击的对象

  • 为了降低 FirstController 代码的复杂度,我们分解它的部分功能到新增的 RoundConrtoller 中实现
public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
    public ActionManager actionManager;
    public DiskFactory diskFactory;                       //飞碟工厂
    RoundController roundController;
    UserGUI userGUI;

    void Start()
    {
        LoadResources();
    }

    public void LoadResources()
    {
        SSDirector.GetInstance().CurrentScenceController = this;
        gameObject.AddComponent<DiskFactory>();
        gameObject.AddComponent<ActionManager>();
        gameObject.AddComponent<PhysisActionManager>();
        gameObject.AddComponent<RoundController>();
        gameObject.AddComponent<UserGUI>();

        diskFactory = Singleton<DiskFactory>.Instance;
        roundController = Singleton<RoundController>.Instance;
        userGUI = Singleton<UserGUI>.Instance;
    }
    
    //处理用户的点击动作
    public void Hit(Vector3 position)
    {
        Camera ca = Camera.main;
        Ray ray = ca.ScreenPointToRay(position);

        RaycastHit[] hits;
        hits = Physics.RaycastAll(ray);

        for (int i = 0; i < hits.Length; i++)
        {
            RaycastHit hit = hits[i];
            //如果用户点击到飞碟
            if (hit.collider.gameObject.GetComponent<DiskData>() != null)
            {
                //将飞碟移至底部,触发飞行动作的回调
                hit.collider.gameObject.transform.position = new Vector3(0, -7, 0);
                //分数增加
                roundController.Record(hit.collider.gameObject.GetComponent<DiskData>());
                //更新GUI
                userGUI.SetPoints(roundController.GetPoints());
            }
        }
    }
    
    //重新开始
    public void Restart()
    {
        userGUI.SetResult("");
        userGUI.SetPoints(0);
        userGUI.SetRound(0);
        userGUI.SetMode(1);
        userGUI.SetFlyMode(0);
        roundController.Reset();
    }
    
    //设置模式
    public void SetMode(int mode)
    {
        roundController.SetMode(mode);
    }

    public void SetFlyMode(int flyMode){
        roundController.SetFlyMode(flyMode);
    }

    public void FreeDisk(GameObject disk){
        diskFactory.FreeDisk(disk);
    }

    void Update()
    {
       
    }
}
6)新增 PhysisFlyAction.cs
  • 实现物体的物理学飞行
  • 当不使用物理引擎时启用 isKinematic 运动学变换,当使用物理引擎时关闭运动学变换
  • 给飞碟预制体添加刚体属性,选择Use Gravity,最后增加一个初速度,飞碟就会自动实现重力和碰撞效果
  • 利用的是物体的刚体属性,以及物理引擎实现物理运动

3D游戏编程与设计 HW 6 物理系统与碰撞

public class PhysisFlyAction : SSAction
{
    float speed;            //初始速度
    Vector3 direction;      //飞行初始方向

    //生产函数
    public static PhysisFlyAction GetSSAction(Vector3 direction, float speed)
    {
        PhysisFlyAction action = ScriptableObject.CreateInstance<PhysisFlyAction>();
        action.speed = speed;
        action.direction = direction;
        return action;
    }

    public override void Start()
    {
        //设置刚体属性
        gameObject.GetComponent<Rigidbody>().isKinematic = false;
        gameObject.GetComponent<Rigidbody>().velocity = speed * direction;
    }

    public override void Update()
    {
        //如果飞碟到达底部,则动作结束,进行回调
        if (this.transform.position.y < -6)
        {
            this.destroy = true;
            this.enable = false;
            this.callback.SSActionEvent(this);
        }

    }
}
7)新增 PhysisActionManager.cs
  • 物理模式的动作管理类,代码与 ActionManager 类似
public class PhysisActionManager : SSActionManager, ISSActionCallback, IActionManager
{
    PhysisFlyAction flyAction;
    FirstController controller;

    protected new void Start()
    {
        controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
    }

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

    //回调函数,飞碟结束飞行后进行回收
    public void SSActionEvent(SSAction source,
                              SSActionEventType events = SSActionEventType.Competed,
                              int intParam = 0,
                              string strParam = null,
                              Object objectParam = null)
    {
        controller.FreeDisk(source.gameObject);
    }
}
8)新增 RoundController.cs
  • RoundController 实现的是 Hit UFO 1.0 中 FirstController 每回合发送飞碟、计分、计轮次等功能的代码
public class RoundController : MonoBehaviour
{
    FirstController controller;
    IActionManager actionManager;                   //动作管理者
    DiskFactory diskFactory;                         //飞碟工厂
    UserGUI userGUI;
    int[] roundDisks;           //对应轮次的飞碟数量
    int mode;                   //当前模式,简单-0,正常-1,困难-2,或者无限模式-3
    int points;                 //当前分数
    int round;                  //当前轮次
    int sendCount;                //当前已发送的飞碟数量
    float sendTime;             //发送时间

    void Start()
    {
        controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
        actionManager = Singleton<ActionManager>.Instance;
        diskFactory = Singleton<DiskFactory>.Instance;
        userGUI = Singleton<UserGUI>.Instance;
        roundDisks = new int[] { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 };
        mode = 1;
        points = 0;
        round = 1;
        sendCount = 0;
        sendTime = 0;
    }

    public void Reset()
    {
        mode = 1;
        points = 0;
        round = 1;
        sendCount = 0;
        sendTime = 0;
    }

    public void Record(DiskData disk)
    {
        points += disk.points;
    }

    public int GetPoints()
    {
        return points;
    }

    public void SetMode(int mode)
    {
        this.mode = mode;
    }

    public void SetFlyMode(int flyMode)
    {
        actionManager = flyMode == 1 ? Singleton<PhysisActionManager>.Instance : Singleton<ActionManager>.Instance as IActionManager;
    }

    public void SendDisk(int mode)
    {
        //从工厂生成一个飞碟
        GameObject disk = diskFactory.GetDisk(round);
        //设置飞碟的随机位置
        disk.transform.position = new Vector3(-disk.GetComponent<DiskData>().direction.x * 7, UnityEngine.Random.Range(0f, 8f), 0);
        disk.SetActive(true);
        //设置飞碟的飞行动作
        if (mode == 0){
            actionManager.Fly(disk, disk.GetComponent<DiskData>().speed*0.5f, disk.GetComponent<DiskData>().direction);
        }
        else if (mode == 2) {
            actionManager.Fly(disk, disk.GetComponent<DiskData>().speed*1.5f, disk.GetComponent<DiskData>().direction);
        }
        else {
            actionManager.Fly(disk, disk.GetComponent<DiskData>().speed, disk.GetComponent<DiskData>().direction);
        }
        
    }

    // Update is called once per frame
    void Update()
    {
        sendTime += Time.deltaTime;
        //每隔1s发送一次飞碟
        if (sendTime > 1)
        {
            sendTime = 0;
            //每次发送至多5个飞碟
            for (int i = 0; i < 5 && sendCount < roundDisks[round-1]; i++)
            {
                sendCount++;
                SendDisk(mode);
            }
            //判断是否需要重置轮次,不需要则输出游戏结束
            if (sendCount == roundDisks[round-1] && round == roundDisks.Length)
            {
                if (mode == 3)
                {
                    round = 1;
                    sendCount = 0;
                    userGUI.SetResult("");
                }
                else
                {
                    userGUI.SetResult("Game Over!");
                }
            }
            //更新轮次
            if (sendCount == roundDisks[round-1] && round < roundDisks.Length)
            {
                sendCount = 0;
                round++;
                userGUI.SetRound(round);
            }
        }
    }
}

二、Model

  • Model 的三个代码文件都不需要更改,直接复用 Hit UFO 基础版的代码

3D游戏编程与设计 HW 6 物理系统与碰撞

三、 View

3D游戏编程与设计 HW 6 物理系统与碰撞

1)修改 UserGUI.cs
  • 界面类,用于构建 UI 和捕捉用户动作,将分数,Round,游戏模式显示出来
  • 新增两个设置运动模式的按钮,且将UserGUI的成员设置成了私有
public class UserGUI : MonoBehaviour
{
    IUserAction userAction;
    string result;
    int points;
    int round;
    int mode;
    int flyMode;

    public void SetResult(string result){
        this.result = result;
    }

    public void SetPoints(int points){
        this.points = points;
    }

    public void SetRound(int round){
        this.round = round;
    }

    public void SetMode(int mode){
        this.mode = mode;
    }

    public void SetFlyMode(int flyMode){
        this.flyMode = flyMode;
    }

    void Start()
    {
        result = "";
        points = 0;
        round = 1;
        mode = 1;
        flyMode = 0;
        userAction = SSDirector.GetInstance().CurrentScenceController as IUserAction;
    }
    
    //打印和用户交互提示界面
    void OnGUI()
    {
        GUIStyle titleStyle = new GUIStyle();
        titleStyle.normal.textColor = Color.black;
        titleStyle.fontSize = 50;

        GUIStyle style = new GUIStyle();
        style.normal.textColor = Color.white;
        style.fontSize = 30;

        GUIStyle resultStyle = new GUIStyle();
        resultStyle.normal.textColor = Color.red;
        resultStyle.fontSize = 50;

        GUIStyle modeStyle = new GUIStyle();
        modeStyle.normal.textColor = Color.blue;
        modeStyle.fontSize = 40;

        GUI.Label(new Rect(600, 30, 50, 200), "Hit UFO", titleStyle);
        GUI.Label(new Rect(20, 10, 100, 50), "Points: " + points, style);
        GUI.Label(new Rect(220, 10, 100, 50), "Round: " + round, style);
        GUI.Label(new Rect(1000, 100, 50, 200), result, resultStyle);

        if (GUI.Button(new Rect(1300, 50, 100, 50), "Restart"))
        {
            userAction.Restart();
        }
        //简单模式
        if (GUI.Button(new Rect(1300, 125, 100, 50), "Easy Mode"))
        {
            userAction.SetMode(0);
        }
        //通常模式
        if (GUI.Button(new Rect(1300, 200, 100, 50), "Normal Mode"))
        {
            userAction.SetMode(1);
        }
        //困难模式
        if (GUI.Button(new Rect(1300, 275, 100, 50), "Hard Mode"))
        {
            userAction.SetMode(2);
        }
        //无限模式
        if (GUI.Button(new Rect(1300, 350, 100, 50), "Infinite Mode"))
        {
            userAction.SetMode(3);
        }
        //运动学模式
        if (GUI.Button(new Rect(1300, 425, 100, 50), "Kinematics"))
        {
            userAction.SetFlyMode(0);
        }
        //物理模式
        if (GUI.Button(new Rect(1300, 500, 100, 50), "Physis"))
        {
            userAction.SetFlyMode(1);
        }

        //捕捉鼠标点击
        if (Input.GetButtonDown("Fire1"))
        {
            userAction.Hit(Input.mousePosition);
        }
    }
}

3. 游戏界面

  • 调整了飞碟预制的形状和大小,让它相比之前看起来更像是飞碟

3D游戏编程与设计 HW 6 物理系统与碰撞

  • 物理运动模式下游戏画面,飞碟之间会互相碰撞

3D游戏编程与设计 HW 6 物理系统与碰撞

相关标签: 游戏 游戏开发