物理系统与碰撞

物理系统与碰撞

PS:所有代码请点击下方代码传送门(飞碟与打靶都在里面)
代码传送门



1、改进飞碟(Hit UFO)游戏:

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

  下面是adapter模式的 UML类图:
UML

题目理解

  对于本次游戏要求,我觉着本质上就是一个要求。为啥这么说咧?咱先卖个关子,说下我对于 adapter、proxy 以及 facade 模式的理解。非常欢迎指正······我是真的理解的不太行,太菜了

adapter:适配器模式

  什么是适配器模式呢?它的作用是将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。

proxy:代理模式

  其功能是为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。

facade:外观模式

  为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。

三者区别

  先从字面意义上去简单理解:

  • 代理模式 就是在客户与类之间加了个中间人,客户要拿到类的数据或者修改都需要先告知中间人,然后中间人去调用类的方法去获取或修改数据。这样,就可以起到限制客户直接访问类的作用,对于数据保护提供了一定安全保障。
  • 适配器模式 关键就是 适配 嘛,注重的是 转换与调整。我脑补的例子是有可能有多个不同的类实现了相似的某种功能,但有些许的不同,所以我们需要根据需求去使用不同的类所实现的函数。这样,我们就需要提供一个接口,使得我们只需要根据简单地修改输入参数就能获取多个类的服务,而不必要去实例化某一个类再使用。
  • 外观模式 也正如其名,外观。对于多个子系统,可能会提供各种各样不同的接口。如果我们不加以封装,使用起来会很容易弄混。因此,我们要封装各个子系统提供的接口,提供一些统一的功能,如查询、修改、删除等。统一之后,各种子系统的使用就会变得清晰明了。

  可能还是有点懵,其实我也有点,尤其是 facade 与 adapter。耐心理解下,或者再搜搜别的博客看看,应该就能有比较好地见解,然后再来指导我呀
  回到最开始的话题,为啥我说两个要求实际上是一个呢?根据 adapter 模式的作用来看,我们要按 adapter 模式修改飞碟游戏,能在哪修改?不就是第二个要求所说的:同时支持物理运动与运动学(变换)运动么!!!有没点道理,哈哈

根据自身理解,与UML图不同的实现

  上边的UML图是老师提供的 adapter 设计图。其实吧,单看这个图,我感受不到 adapter 模式,反而很像另一个东西:继承与多态。有没有,有没有,是不是有同样的感觉!IActionManager 仅仅只是个接口,而 CCActionManager 与 PhysisActionManager 仅仅只是实现了这个接口,然后在场景控制器中修改接口指向的类即可。这不就是多态么?!
  所以我最终决定实现的时候按照我的理解来了,而不是使用老师提供的UML类图。其实思想上差不多,但是我设计的 IActionManager 是一个类,其中包含了两个对象:CCActionManagerPhysisActioinManager。由于两个类都是单例模式,因此两个对象指向的都是两个类的单例。然后,IActionManager 再提供一个函数,输入是调用两个对象中实现的 UFOFly 函数所需要的参数,外加一个用于选择模式的参数:isPhysis。当 isPhysis == 0 时,使用 CCActionManager 中的 UFOFly,isPhysis == 1 时,使用 PhysisActionManager 中的 UFOFly 函数。
  这样的实现与 UML 图中区别不算大,但我认为这样才比较好地提供了一个所谓的适配。下面是设计说明
  两个类的 UFOFly 明显是不可同时使用的,但我在同一件任务(飞碟游戏)中又需要用到二者,因此设计一个适配器类:IActionManager 用于综合两个类。其中仅有一个函数,也命名为 UFOFly,但该函数与 CCActionManager 和 PhysisActionManager 中的 UFOFly 有一点区别,即:

  • IActionManager 中的 UFOFly 多了一个参数:isPhysis 用于判断使用两个类中的哪个 UFOFly 函数。

  这样,客户就可以根据自己的需求简单地设置这个参数,从而获得两个相似但又有所不同的服务。
  逼逼赖赖这么多,接下来就直接介绍怎么做咯。


代码修改部分的实现

  下面介绍会主要介绍此次改进的部分,至于其他部分的详细介绍请看上一篇博客。
博客传送门

Models

  Models变化不大,就在原基础上给 IUserAction 加多了一个函数:setFlyMode()。

	public interface IUserAction{
        // get player's score
        int GetScore();

        // get round number
        int GetRound();

        // 新加函数
        // set fly mode: 0: 使用运动学(变换);  1: 使用物理运动;
        void setFlyMode(int flyMode);

        // Game start
        void GameStart();

        // Game Over
        void GameOver();

        // Game restart
        void Restart();

        // Hit The UFO
        void Hit(Vector3 pos);
    }

Actions

  Actions 是此次改动中 比较多且重要的 的部分。下面是改动的部分:

  • 新加了 PhysisAction、PhysisActionManager 以及 IActionManager
  • CCFlyActionManager 在 Start() 处将游戏对象中 刚体组件isKinematic 设置为 true,使得该物体不受外力作用,位置、角度等只受代码中的如 transform.position 等 transform 属性的设置影响。
  • SSAction 中加多了虚函数 FixedUpdate(),因为对刚体的操作需放在 FixedUpdate 中

  下面是这些改动的代码:

SSAction
public class SSAction : ScriptableObject            
{
    public bool enable = true;
    public bool destroy = false;
    public GameObject gameobject;
    public Transform transform;
    public ISSActionCallback callback;

    protected SSAction() { }                        // 防止用户自己new抽象的対象                      
    
    public virtual void Start(){                    // 申明虚方法,通过重写实现多态
        throw new System.NotImplementedException();
    }

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

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

SSAction 仅仅是添加了个虚函数

CCUFOFlyAction
public override void Start(){
    // 不做任何事情
    // 新加代码
    gameobject.GetComponent<Rigidbody>().isKinematic = true;
}

CCUFOFlyAction 将刚体设置为使用运动学,而非实际物理运动(受外力影响)

PhysisAction
public class PhysisAction : SSAction{
    public int side;
    public float x_speed = 0.0f;

    private PhysisAction() { }
    public static PhysisAction GetSSAction(int s, float start){
        PhysisAction action = ScriptableObject.CreateInstance<PhysisAction>();//让unity创建动作类,确保内存正确回收
        action.side = s;
        if(action.side==-1)    // -1: left side;  1: right side
            action.x_speed = start;
        else
            action.x_speed = -start;
        return action;
    }

    public override void Update(){
        
    }

    public override void FixedUpdate(){
        if (this.transform.position.y<90 || this.transform.position.z<-15 || this.transform.position.z>15){
            // waiting for destroy
            this.destroy = true;
            this.callback.SSActionEvent(this);      //告诉动作管理或动作组合这个动作已完成
        }
    }

    public override void Start(){
        gameobject.GetComponent<Rigidbody>().isKinematic = false;
        gameobject.GetComponent<Rigidbody>().velocity = x_speed*new Vector3(0,0,1);
    }
}
  • GetSSAction 与 CCFlyAction 相似
  • FixedUpdate 与 CCFlyAction 中 Update 相似,但无需再计算对象位置等,只需要判断是否超出边界
  • Start 中需要将刚体设置为物理运动的,即受到外力影响。同时,还需要设置物体初速度,因为之前物体速度是利用position位置来模拟的,现在不能直接修改位置,所以得给予物体初速
PhysisActionManager
public class PhysisActionManager : SSActionManager, ISSActionCallback{

    public PhysisAction fly;
    public FirstController sceneController;

    protected new void Start(){
        sceneController = (FirstController)SSDirector.GetInstance().CurrentScenceController;
        // sceneController.action_manager = this;     
    }
    
    public void UFOFly(GameObject UFO, int side, float start_speed){
        fly = PhysisAction.GetSSAction(side,start_speed);
        this.RunAction(UFO, fly, this);
    }

    #region ISSActionCallback implementation
    public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
        int intParam = 0, string strParam = null, Object objectParam = null){
        // 不做任何事情
        return;
    }
    #endregion
}

该类与 CCActionManager 相似,仅仅是把 CCFlyAction 换成 PhysisAction

IActionManager
public class IActionManager : MonoBehaviour{
    public CCFlyActionManager fly_manager;
    public PhysisActionManager physis_manager;
    public FirstController sceneController;

    public void Start(){
        sceneController = (FirstController)SSDirector.GetInstance().CurrentScenceController;
        fly_manager = Singleton<CCFlyActionManager>.Instance;
        physis_manager = Singleton<PhysisActionManager>.Instance;
    }

    public void UFOFly(GameObject UFO, int side, float start_speed, int isPhysis){
        if(isPhysis == 1){
            physis_manager.UFOFly(UFO,side,start_speed);
        }
        else{
            fly_manager.UFOFly(UFO,side,start_speed);
        }
    }
}

fly_manager 和 physis_manager 为对应类的单例
sceneController 即 FirstController
UFOFly 中只是简单的条件判断,根据 isPhysis 值使用不同的 UFOFly


FirstController

  场景控制器修改内容如下:

  • 添加了 PhysisActionManager 与 IActionManager,并进行了初始化
  • 添加了 isPhysis 变量,用于判断使用 CCFlyActionManager 还是 PhysisActionManager 中的 UFOFly
  • SendDisk 改为使用 IActionManager 中的 UFOFly 函数
  • 额外实现了 setFlyMode() 函数,用于设置 isPhysis
变量
// 新加变量
private int isPhysis = 0; 
public PhysisActionManager paction_manager;
public IActionManager iaction_manager;
初始化
public void Start (){
    SSDirector director = SSDirector.GetInstance();
    director.CurrentScenceController = this;  
    ccaction_manager = gameObject.AddComponent<CCFlyActionManager>() as CCFlyActionManager;
    paction_manager = gameObject.AddComponent<PhysisActionManager>() as PhysisActionManager;
    iaction_manager = gameObject.AddComponent<IActionManager>() as IActionManager;
    disk_factory = gameObject.AddComponent<DiskFactory>();
    score_recorder = gameObject.AddComponent<ScoreRecorder>();
    user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;
    game_state = 0;
}
SendDisk 修改
private void SendDisk(){                   
    if(disk_queue.Count != 0){
        GameObject disk = disk_queue.Dequeue();
        disk_send.Add(disk);
        disk.SetActive(true);

        // 设置飞碟发送前的初始位置
        float ran_y = Random.Range(98.0f, 106.0f);
        float ran_z = Random.Range(-1f, 1f) < 0 ? -12.6f : 12.6f;
        if(ran_z<0)
            disk.GetComponent<DiskData>().appear_side = -1;
        else
            disk.GetComponent<DiskData>().appear_side = 1;
        disk.transform.position = new Vector3(0, ran_y, ran_z);
        // 向动作管理器发起动作
        float speed = Random.Range(13.0f,16.0f);
        
        // 修改部分
        iaction_manager.UFOFly(disk,disk.GetComponent<DiskData>().appear_side,speed,isPhysis);
    }

    // 飞碟飞出视野后进行回收以及玩家扣血等操作
    for (int i = 0; i < disk_send.Count; i++){
        GameObject d = disk_send[i];
        if ((d.transform.position.y < 92 || d.transform.position.z<-13 || d.transform.position.z>13)&& d.gameObject.activeSelf == true){
            disk_factory.FreeDisk(d);
            disk_send.Remove(d);
            user_gui.DecBlood();
        }
    }
}
setFlyMode
public void setFlyMode(int flyMode){
    isPhysis = flyMode;
}

UserGUI

  UserGUI 的修改其实很简单,就只是在 游戏开始 以及 游戏结束 界面加多两个按钮,用于选择飞碟的 Fly 模式。下面只放了修改部分的代码,其它代码可在项目中或上一篇博客中看到。

void OnGUI (){
    // 设置style
    ······

    if (game_start){
        // 用户点击进行射击
        ······

        // 显示回合数、分数
        ······
        
        // 显示玩家血量
        ······

        // 游戏结束的操作
        if (HP <= 0){
            ······
            if (GUI.Button(new Rect(Screen.width / 10, Screen.height / 2-40, 200, 50), "运动学(变换)模式")){
                action.setFlyMode(0);
            }
            if (GUI.Button(new Rect(Screen.width / 10, Screen.height / 2+40, 200, 50), "物理运动模式")){
                action.setFlyMode(1);
            }
            action.GameOver();
        }
    }
    else{
        ······
        if (GUI.Button(new Rect(Screen.width / 10, Screen.height / 2-40, 200, 50), "运动学(变换)模式")){
            action.setFlyMode(0);
        }
        if (GUI.Button(new Rect(Screen.width / 10, Screen.height / 2+40, 200, 50), "物理运动模式")){
            action.setFlyMode(1);
        }
    }
}



实验结果

  本次的实验其实难度相对较小,主要是添加了对刚体的编程操作,以及对新模式的学习。刚体和碰撞体在游戏中我认为可以说是最常用的组件,因此,熟悉这些组件各个属性的作用,以及如何使用代码去控制是非常重要的一个学习任务。
  好咯,不吹水了,最后的任务:上图!

游戏开始:
GameStart
游戏中:
Playing
游戏结束:
GameOver
游戏动态效果:
运动学变换模式:
KinematicMode
物理运动模式:
PhysisMode
  以上就是本次实验的动画结果啦,跟上次变化不大,其实就是多了两个按钮,两种运动模式。

2. 打靶游戏(可选作业):

游戏内容要求:

  • 靶对象为 5 环,按环计分;
  • 箭对象,射中后要插在靶上
    • 增强要求:射中后,箭对象产生颤抖效果,到下一次射击 或 1秒以后
  • 游戏仅一轮,无限 trials;
    • 增强要求:添加一个风向和强度标志,提高难度

最终结果

游戏开始:
GameStart
游戏中:
Playing
游戏结束:
GameOver
游戏效果展示:
PlayGame

游戏设计

  我的设计满足了上述要求中的下列部分:

  • 靶对象为 5 环,按环计分
  • 箭对象,射中后要插在靶上
  • 游戏仅一轮
    • 增强要求:添加一个风向和强度标志,提高难度

  其中,我觉得 无限 trials 的规则略微有些不合理,所以我设计的是只有五支箭。

游戏预制设计

Prefabs
预制

箭矢设计

  先创建一个空对象,然后在对象下创建一个圆柱体和一个方块,方块作为箭头,圆柱体作为箭身。其中,空对象要加 Rigidbody 组件,箭头的 collider 要勾选 Is Trigger。

箭身设计

  创建一个空对象,在其下创建五个圆柱体。然后每个圆柱体加入一个 Mesh Collider 组件,并且勾选 Convex。我原本想着每个圆柱体只稍微突出一点,使得肉眼看不出这是五个圆柱体,但我不知道为什么最终碰撞时总是只识别到一个环,所以我最后就分的很开,像上图右边那样,很丑。。。

主要编程部分

  此次设计中,没有设计动作管理器,因为就只是使用了物理引擎,没有必要专门再去设计。

Models
public interface IUserAction{
    void CreateArrow();
    void FollowMouse(Vector3 mousePos);
    void Shoot(Vector3 mousePos);
    int GetScore();
    int GetWindDirection();
    int GetWindStrength();
    bool IsGameOver();
    bool isHoldingArrow();
}

主要是IUserAction的设计,包括创造箭矢,箭矢跟踪鼠标、射出箭矢、获取分数、获取风向、获取风力大小、判断游戏是否结束、是否还握着箭矢(协助判断GameOver)。

ArrowFactory

  主要有以下四个函数:

  • GetArrow():生产箭矢
  • IsAllFree():判断是否所有箭矢都中靶或回收,用于协助判断游戏结束时机,否则可能会提前弹出游戏结束界面
  • FreeArrow():回收飞出视野的箭矢
  • Reset():重置
FirstController

  根据需求实现 IUserAction 以及 ISceneController 的接口即可。

ArrowCollider

  可以说是最重要的部分之一。主要是实现箭头检测到碰撞后将箭头设置为不可见,并将父对象 Arrow 的 Rigidbody 组件中的 IsKinematic 属性设置为 true,使得箭身能固定在靶子上。

public class ArrowCollider : MonoBehaviour
{
    public ScoreRecorder score_recorder;
    // Start is called before the first frame update
    void Start()
    {
        score_recorder = Singleton<ScoreRecorder>.Instance;
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    void OnTriggerEnter(Collider c){
        if(c.gameObject.tag == "target"){
            gameObject.transform.parent.gameObject.GetComponent<Rigidbody>().isKinematic = true;
            gameObject.SetActive(false);     //这样设置是为了防止多次触发

            int point = c.gameObject.name[c.gameObject.name.Length - 1] - '0';
            score_recorder.Record(point);
        }
    }
}

  具体效果请到 github 中下载来试下咯。地址在顶部。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值