Box2DSharp使用手册#1

  1.7 C#, 3.2 游戏引擎技术
  • #1部分只围绕碰撞检测进行先关的技术总结,以及不确定有没有#2。
  • GitHub网址:传送门,Github里有群,群里面有中文教程PDF
  • 配置环境:Unity2021.f1c1+VS2022
  • 需要前置知识:高中物理

简介

  • 原生Box2D是使用C++进行编译的物理引擎库。他可以不依赖于任何其他环境进行独立的物理行为模拟。使用该物理引擎库最出名的游戏应该是《愤怒的小鸟》
  • GitHub有Box2DSharp(C#代码版本)

安装&兼容性

  • Box2DSharp是Box2D的C#版本,基本可以在各个IDE环境中进行使用。
  • Box2DSharp的工程文件依赖于Unity2019版本,也就是说如果你使用的不是Unity那么反而可以安心的使用Box2DSharp,而你想要在更高版本的Unity中使用就要解决其兼容性问题。

安装

  • 首先拷贝工程到一个非Unity工程目录下的文件夹中,然后运行CopyToUnityTestbed.bat文件。
  • 第二把test/UnityTest/拷贝到工程文件夹中。(原则上来说到这一步安装就算完成了)

兼容性&各类报错

  • 首先是unsafe(注意区分大小写)报错,会有很明显的中文提示让你打开编译器的unsafe开关。
  • 第二,是ImGuiNet报错。该项目的TestDemo的GUI使用的是这个,所以如果你不想要Demo可以把Demo删除只保留UnityTest\Assets\Box2DSharp文件夹即可。
    解决该问题的方法为,在Unity中导入Git仓库:传送门
RVRI9NFA1D6TK41ZY6W - Box2DSharp使用手册#1
  • 如果在导入Git仓库中出现报错,且一导入键就直接报错,那么大概率是Git的问题。你需要先安装Git其次要把Git的:git\cmd文件夹目录添加到电脑系统变量的Path中。
    可以通过Win+R->cmd->输入git来查看验证。
  • 第三,会有InputSystem的报错,可以在PackageManager中导入新的InputSystem。如果使用了原生版本的输入系统,那么需要在playersetting中找到对应输入系统选项并改为Both。
  • 第四,也是最重要的一点。在Unity2019以上的版本中使用会出现Unsafe(这里是大写方法名)报错,该方法在System.Rutime.CompilerServices.Unsafe.dll中,你甚至可以在Unity本体的Editor中查找到该文件。
    但是在更新版本的Unity中,Unity在Assembly的引用中并没有把该dll加入引用集中,这导致你直接用VS打开他自己的sln文件时VS不会报错,但用Unity的工程sln中就会出现Unsafe方法名报错。且你不可以通过把UnityEditor中的该dll以放入到Plugin文件夹下或者Unity的Nuget插件或者smcs.rsp强制引用等任何你能想到的方式来引用加载啊该dll。
    解决方法,通过Unity打开VS工程文件(不要直接点击sln),然后用VS的Nuget(工具->Nuget包管理->管理解决方案的Nuget程序包)搜索并下载System.Rutime.CompilerServices.Unsafe(一定要是最新的稳定版)。之后,你可以在你的工程下的Package中找到VS安装的这个dll文件,然后把该dll文件拷贝到Assets/Plugins的文件夹下,等待Unity重新编译即可解决该问题。
    (如果仍未解决可以尝试在Assets的文件夹目录下用文本文档新建smcs.rsp文件,然后加入语句“-r:System.Runtime.CompilerServices.Unsafe.dll“)
  • 其它可能影响失败的因素:VS没有开启unsafe代码编译,该问题需要对Assebly右键->属性。但是Vs默认对Unity不显示属性文件,所以你会发现点击后无事发生。此时需要在VS上方选项栏中选择工具->选项->适用于Unity的工具,并把其中的访问项目属性改为True,之后对于你需要的Assembly中右键属性,勾选允许使用不安全代码开关。
image 21 - Box2DSharp使用手册#1
image 22 1024x497 - Box2DSharp使用手册#1

入门教程

核心概念

  • 世界(world):物理世界就是相互作用的物体,夹具和约束的集合。(一般只用创建一个)
  • 求解器(solver):物理世界使用求解器来推算世界,求解接触和关节约束。
  • Box2D 的求解器是一种高性能的迭代求解器,它会顺序执行 N 次,这里的 N 是约束的个数。
  • 连续碰撞(continuous collision):求解器使用时域上的离散时间步来推算物体状态。如果没有特殊处理的话,这会导致隧穿效应。
  • 形状(shape)
  • 刚体(rigid body):区别于流体来说
  • 夹具(fixture):夹具将形状绑定到物体上,并添加密度(density)、摩擦(friction)、恢复(restitution)等材料特性。夹具还将形状放入到碰撞系统(碰撞检测(Broad Phase))中以使之能与其他形状相碰撞。
  • 约束(constraint):消除物体的自由度链接(xyz轴的约束)。
  • 接触约束(contact constraint):一种防止刚体穿透,并模拟摩擦和恢复的特殊约束。
  • 关节(joint):它是一种用于把两个或更多的物体固定到一起的约束。
  • 关节限制,关节马达:本节不作过多介绍

运行官方Demo

  • 成功运行后的Demo界面(直接把Test场景拖入并运行即可)
image 23 - Box2DSharp使用手册#1
  • 参数1解释:
    Vel(Velocity):对碰撞物体碰撞后重新分配物理属性(质量、速度、方向等)的矫正次数,一般为10即可满足正常需求。数值越大精度越高且精确。
    Pos(Position):碰撞矫正,发生碰撞后物体会发生一定程度的重叠,此时Box2D会对其进行矫正。一般为一般为10即可满足正常需求。数值越大精度越高且精确。
    Hertz:赫兹
    Sleep:是否标记沉睡物体。开启时,会把标记为sleep的物体跳过计算,以此来节省CPU计算消耗。
    Warm Starting:热启动,用于调试求解器(经实测建议开启此项,否则可能会遇到一些意料之外的错误)
    Time of Impact:按周期计算穿梭碰撞。(Compute the upper bound on time before two shapes penetrate,这里没太理解upper bound只是是谁的上限)
    Sub-Stepping:子步进,用于调试。
  • 参数2解释:
    Shapes:是否绘制形状
    Joints:是否显示/计算关节
    AABBs:是否使用AABB计算边界
    (中间那几个暂时用不到)
    Profile:性能统计

基础代码

  • 一个标准的Box2D代码有:创建一个世界->创建一些物体->把物体用夹具进行绑定->开始进行物理模拟
  • 如何创建世界(注意:以下所有关于Box2D的Vector2都是System.Numerics里的Vector2)
public World world;
void Start()
{
//实例化一个世界时需要给他填入重力Vector2数组
        world = new World(new Vector2(0f,0f));
//调整相关参数,这个上文里说过,也可以先通过Profile profile = new Profile();然后更改里面的设定,之后动过profile.xxx进行赋值。
        world.AllowSleep = true;
        world.WarmStarting = true;
        world.ContinuousPhysics = true;
        world.SubStepping = true;
}
  • 如何画一个圆形/方形
Body ground;
{
    var bd = new BodyDef();
    ground = World.CreateBody(bd);
}

var bd = new BodyDef
{
    BodyType = BodyType.DynamicBody,
    AllowSleep = false,
    Position = new Vector2(0.0f, 10.0f)
};

//圆形
var shape = new CircleShape();
shape.Radius = 10f;
shape.Position.Set(0, 0);
body.CreateFixture(shape, 0);

//方形
 var shape = new PolygonShape();
shape.SetAsBox(0.5f, 10.0f, new Vector2(10.0f, 0.0f), 0.0f);
body.CreateFixture(shape, 5.0f);
  • 如何进行Box2D body和U3D场景中的GameObject对应更新
//官方演示demo中并没有使用body位置进行更新,而是通过夹具的碰撞接触点进行图案的绘制,所以当你看完官方演示之后反而会更糊涂了。
//而且Box2D并没有返回id值记录的相关操作,这方面我们需要自己进行维护,这里我直接采用字典进行维护对应关系
private Dictionary<Body, GameObject> objcectList;
//创建一个GameObject,紧随其后创建对应shap和body,然后把body和GameObject塞入字典中,之后用foreach遍历更新即可

//最后,在update中调用。第一个参数填入刷新频率,此时的分母相当于帧数,第二个填入Vel计算次数,第三个填入pos计算次数。经实测两个填入3就有不错的效果。
world.Step(1.0f / 10.0f, 3, 3);
  • 额外补充:
    1、如果你想测试旋转角度,官方示例中推荐的Density为1,这会导致如果你添加力过小的话,物体发生旋转的角度可以忽略不计。你需要更改这方面的内容。(可以设置一个Density不为零的物体到非常远的地方,或者不放入到字典中,之后其余的物体Density=0。官方手册中说明至少要有一个物体Density不为零否则会导致奇怪的计算出现)
    2、物体中有一些其他的属性,例如摩擦力等,都有在手册中写到

完整示例代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Box2DSharp.Collision.Shapes;
using Box2DSharp.Common;
using Box2DSharp.Dynamics;
using Box2DSharp.Dynamics.Joints;
using Box2DSharp.Collision;
using Testbed.Abstractions;
using System;
using Joint = Box2DSharp.Dynamics.Joints.Joint;
using Vector2 = System.Numerics.Vector2;
using Color = Box2DSharp.Common.Color;
using Random = UnityEngine.Random;

using UnityEngine.UI;
using Box2DSharp.Dynamics.Contacts;
using Box2DSharp.Collision.Collider;

public class CirlTest : MonoBehaviour
{
    // Start is called before the first frame update
    [SerializeField]
    private GameObject _prefab;
    [SerializeField]
    private GameObject _squarePrefab;

    [SerializeField]
    private UnityEngine.Transform _transform;
    public World world;
    private int cnt = 300;
    private int now_cnt;

    private Dictionary<Body, GameObject> objcectList;

    [Range(0f, 1f)]
    public float speed = 0.1f;

    //show fps
    private float m_frameCount = 0.0f;
    private string m_fpsText;
    private float m_lastPrintTime = 0.0f;
    private GUIStyle m_style;

    [SerializeField]
    private Text m_text;
    private bool flag;


    void Start()
    {
        world = new World(new Vector2(0f,0f));
        world.AllowSleep = true;
        world.WarmStarting = true;
        world.ContinuousPhysics = true;
        world.SubStepping = true;

        //创建地面
        var groundBodyDef = new BodyDef { BodyType = BodyType.StaticBody };
        groundBodyDef.Position.Set(0.0f, -10.0f);
        var groundBody = world.CreateBody(groundBodyDef);

        objcectList = new Dictionary<Body, GameObject>();
        objcectList.Clear();

        now_cnt = 0;

        //show fps
        m_style = new GUIStyle { fontSize = 20, };



    }

    // Update is called once per frame
    private void FixedUpdate()
    {
       
        calFPS();
        if (flag == false) return;

        UpdatePosition();
    }

    protected void PreStep()
    {
        float rx = Random.Range(-50.0f, 50.0f);
        float ry = Random.Range(-50.0f, 50.0f);

        if(now_cnt >= 30)
        {
            //创建实例化对象
            var circle = GameObject.Instantiate(_prefab, new UnityEngine.Vector2(rx, ry), Quaternion.identity);
            circle.transform.SetParent(_transform);

            //在world中对应创建对象
            var bodyDef = new BodyDef { BodyType = BodyType.DynamicBody };

            var shape = new CircleShape();
            shape.Radius = 0.95f;
            bodyDef.Position.Set(rx, ry);


            var body = world.CreateBody(bodyDef);
            body.CreateFixture(shape, 1.0f);

            objcectList.Add(body, circle);
        }
        else
        {
            //创建实例化对象
            var square = GameObject.Instantiate(_squarePrefab, new UnityEngine.Vector2(rx, ry), Quaternion.identity);
            square.transform.SetParent(_transform);
            //square.transform.localScale = new Vector3(, 1, 1);

            var bd = new BodyDef
            {
                BodyType = BodyType.DynamicBody,
                Position = new Vector2(rx, ry),
                FixedRotation = false
        };
            var body = world.CreateBody(bd);


            var shape = new PolygonShape();
            shape.SetAsBox(1f, 2f);

            var fixtureDef = new FixtureDef
            {
                Shape = shape,
                Density = 0.001f,
                Friction = 0.3f
            };

            //body.ApplyForceToCenter(new Vector2(-rx/10, -ry/10), true);
            body.AngularDamping = 0.01f;
            body.CreateFixture(fixtureDef);


            objcectList.Add(body, square);
        }

        ++now_cnt;
    }

    private void UpdatePosition()
    {
        foreach (var obj in objcectList)
        {
            Vector2 pos = obj.Key.GetPosition();
            float angle = UnityEngine.Vector2.SignedAngle(new UnityEngine.Vector2(pos.X,pos.Y), UnityEngine.Vector2.left);
            obj.Key.SetTransform(pos - pos * speed * Time.deltaTime, angle);
        }

        world.AllowSleep = true;
        world.WarmStarting = true;
        world.ContinuousPhysics = true;
        world.SubStepping = true;

        if (now_cnt < cnt)
        {
            PreStep();
        }

        world.Step(1.0f / 10.0f, 3, 3);

        foreach (var obj in objcectList)
        {
            Vector2 pos =  obj.Key.GetPosition();
            obj.Value.transform.position = new UnityEngine.Vector2(pos.X,pos.Y);
            obj.Value.transform.rotation = Quaternion.Euler(new Vector3(0,0, obj.Key.GetAngle()));
            //if (obj.Key.GetAngle() >= 1f) Debug.Log(obj.Key.GetAngle());
        }
    }

    //show fps
    private void calFPS()
    {
        m_frameCount += 1.0f;
        float currentTime = UnityEngine.Time.realtimeSinceStartup;
        float passedTime = currentTime - m_lastPrintTime;
        //每秒钟计算一次fps
        if (passedTime >= 1.0f)
        {
            float fps = m_frameCount / passedTime;
            m_fpsText = string.Format("FPS:{0:0.00}", fps);
            m_frameCount = 0.0f;
            m_lastPrintTime = currentTime;
        }
    }

    void OnGUI()
    {
        if (string.IsNullOrEmpty(m_fpsText) == false)
        {
            GUI.Label(new Rect(20.0f, 0.0f, 300.0f, 20.0f), m_fpsText, m_style);
        }
    }

    public void OnClickedBtn()
    {
        cnt =  int.Parse(m_text.text);
        flag = true;
    }
}

One Reply to “Box2DSharp使用手册#1”

LEAVE A COMMENT