# 前言

上文 关于手游战斗系统 提到了作为一个独立的战斗系统,前后端可能都是需要运行的,因此物理系统自然不能使用前端引擎自带的,本文介绍了一下 Box2DSharp —— 一个 Box2D 的 C# 版本。

# 简介

在 Box2D 中,一个 World 代表一个物理世界。

其中,一个完整物体,分为 Body、Shape、Fixture

  • Body:代表一个对象
  • Shape:形状
  • Fixture:负责将形状 Shape 与对象 Body 连接

一个 World 需要实际『动』起来,就需要调用其 Step 进行更新,Step 方法接收三个参数,分别为:

  • timeStep:物理更新间隔,类似于 Unity 的 Time.deltaTime
  • velocityIterations:速度迭代解算值
  • positionIterations:位置迭代解算值

参考 - Simulating the World

The suggested iteration count for Box2D is 8 for velocity and 3 for position. You can tune this number to your liking, just keep in mind that this has a trade-off between performance and accuracy. Using fewer iterations increases performance but accuracy suffers. Likewise, using more iterations decreases performance but improves the quality of your simulation. For this simple example, we don't need much iteration. Here are our chosen iteration counts.

官方推荐 velocityIterations 为 8,positionIterations 为 3,更小的值会减少模拟精度,但有更好的性能,反之亦然。这个值可以取决于使用者对精度与性能的需求,进行调整。

# 基本使用

一个最基础的动态对象创建如下:

private World _world;
private Body _body;
public void Start()
{
    _world = new World(new System.Numerics.Vector2(0, -10));
    _body = _world.CreateBody(new BodyDef()
    {
        Position = new System.Numerics.Vector2(0, 100),
        BodyType = BodyType.DynamicBody,
    });
    var shape = new PolygonShape();
    shape.SetAsBox(1, 1);
    _body.CreateFixture(shape, 0.1f);
}
public void Update()
{
    _world.Step(1 / 60f, 8, 3);
    Console.WriteLine(_body.GetPosition());
}

效果

上述代码在 (0,100) 处创建了一个 1x1 大小的 Box 对象,质量 0.1,运行时将会以 -10 的重力加速度往下掉,由于此处我们并未创建地板,因此对象会一直往下掉,表现在打印的字符串上,值就会越来越小。

此外,这里在更新线程中,并未将 时间与执行间隔 对应起来,相当于 white 类型的一直更新。

所以可以将实际时间间隔加进去:

public const float FrameTime = 1 / 60f;
public const int FrameTimeMis = (int)(FrameTime * 1000);
public const int VelocityIterations = 8;
public const int PositionIterations = 3;
private World _world;
private Body _body;
private double _time;
public void Start()
{
    _world = new World(new System.Numerics.Vector2(0, -10));
    _body = _world.CreateBody(new BodyDef()
    {
        Position = new System.Numerics.Vector2(0, 100),
        BodyType = BodyType.DynamicBody,
    });
    var shape = new PolygonShape();
    shape.SetAsBox(1, 1);
    _body.CreateFixture(shape, 0.1f);
}
public void Update()
{
    _time += FrameTime;
    _world.Step(FrameTime, VelocityIterations, PositionIterations);
    Console.WriteLine($"{_time}: {_body.GetPosition()}");
    Thread.Sleep(FrameTimeMis);
}

如此,运行频率就跟现实的实际的时间挂钩了。

# 带属性物理对象

例如,带 弹性 物体。

在 Box2D 中,物体属性设置位于创建 Fixture 传参 FixtureDef 中:

  • Restitution:弹性
  • Friction:摩擦力

这两个值都是 0~1 之间。另外还有 Density (密度)、IsSensor (类似 Unity 的 IsTrigger)、RestitutionThreshold (弹性阀值,超出阀值才会反弹) 等。

为了测试这一点,接下来,可以创建一个地板,并对上述创建的对象增加一点弹性。

为了方便起见,我将创建物体封装为了一个 单独方法:

public const float FrameTime = 1 / 60f;
public const int FrameTimeMis = (int)(FrameTime * 1000);
public const int VelocityIterations = 8;
public const int PositionIterations = 3;
private World _world;
private Body _body;
private Body _grounder;
private double _time;
public void Start()
{
    _world = new World(new Vector2(0, -10));
    _body = CreateBody(new Vector2(0, 10), 1, 1, BodyType.DynamicBody);
    _grounder = CreateBody(Vector2.Zero, 10, 1, BodyType.StaticBody);
}
public void Update()
{
    _time += FrameTime;
    _world.Step(FrameTime, VelocityIterations, PositionIterations);
    Console.WriteLine($"{_time}: {_body.GetPosition()}");
    Thread.Sleep(FrameTimeMis);
}
private Body CreateBody(Vector2 pos, float sizeX, float sizeY, BodyType bodyType)
{
    Body body = _world.CreateBody(new BodyDef()
    {
        Position = pos,//new Vector2(0, 100),
        BodyType = bodyType,
    });
    var shape = new PolygonShape();
    shape.SetAsBox(sizeX, sizeY);
    body.CreateFixture(new FixtureDef()
    {
        Shape = shape,
        Density = 1f,
        Restitution = 0.5f
    });
    return body;
}

可以看到,该物体在与地面接触之后,被反弹上去,而后又掉下来。

另外的属性例如:根据物体掉落速度到地面的最大速度 (12.5),若将 RestitutionThreshold (弹性阀值) 修改为 13,则该物体就会出现不再反弹的情况:

# 在 Unity3D 中使用

上述只是在控制台程序中进行了一下简单的使用,因为没有可视化,因此也只能看看数值而已,作为物理模拟,若想实际看到效果,就得将其与渲染引擎 链接 起来。

这里直接选择 Unity3D

# 准备

因为不是直接使用 Box2D 原始 C 语言版本,作为 C# 的库,直接将上述代码生成类库,编译成 DLL 放进引擎即可。

这里个人在项目中创建了 Plugins 目录,将编译好的 DLL 放入。


中间稍微还是有些坑的:

  • 首先是平台问题,上面测试时,个人直接创建的 DotNetCore 控制台项目,Box2DSharp 放进来没问题,可以直接使用。但是 Unity3D 不支持 DotNetCore 库,因此创建的库项目必须为 .Net 平台。而 Box2DSharp 中使用到的一些引用,例如 System.Buffers、System.Memery 就必须额外安装模块了:

  • 此外 Box2DSharp 使用过指针代码,因此必须开启『不安全代码选项』,不管是 DLL 项目还是 Unity 设置都是如此。

    其中,不安全代码可能会与 Unity 本身产生冲突,导致编译失败且无法继续进行:

    个人升级了一下 Collections 版本至 0.14.0 解决了这个问题:


新版 Unity 若出现报错,无法加载 pdb 的调试信息,需要修改一下生成设置:

# 测试

然后创建了一个普通的 Box2DTest.cs 测试脚本:

private BattleWorld _battle;
System.Threading.CancellationTokenSource _token = new System.Threading.CancellationTokenSource();
private List<BodyObject> _bodyList = new List<BodyObject>();
void Start()
{
    _battle = new BattleWorld();
    _battle.Start();
    Task.Run(PhysicsWorldUpdate);
    StartCoroutine(UpdatePositon());
}
private void Update()
{
    if (Input.GetMouseButtonDown(0) && !_battle.IsWorldLocked)
    {
        _bodyList.Add(new BodyObject(_battle.CreateBody(new System.Numerics.Vector2(Random.Range(0, 10), 10), 0.5f, 0.5f, BodyType.DynamicBody)));
    }
}
private void OnDestroy()
{
    _token.Cancel();
}
private IEnumerator UpdatePositon()
{
    while (true)
    {
        foreach (var item in _bodyList)
        {
            item.Update();
        }
        yield return null;
    }
}
private void PhysicsWorldUpdate()
{
    while (true)
    {
        if (_token.IsCancellationRequested)
        {
            _battle.Destroy();
            break;
        }
        _battle.Update();
    }
}
class BodyObject
{
    private Body _body;
    private GameObject _obj;
    public BodyObject(Body body)
    {
        _body = body;
        _obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
    }
    public void Update()
    {
        _obj.transform.position = new Vector3(_body.GetPosition().X, _body.GetPosition().Y, 0);
    }
}

如此,就是一个最简单的链接使用了。

# 结语

本文简单介绍了一下 Box2D 结构与使用流程,并将其挪到 Unity 中进行了一个可视化观察。

当然,后续实际使用的话,代码自然还需要进行更好的设计,例如设计中介者将物理系统的相关属性适配出来,而非直接 Unity 脚本就直接读取物理系统内属性,以及优化表现层与物理层物体的更新方式等。

不过本文目的应该是达到了:即个人对 Box2D 使用方式及其概念的熟悉,为后续做帧同步战斗做个前置。

虽然这个物理引擎也不是定点数物理引擎,不过胜在简单,且定点数必然会导致性能下降,个人就在考虑先使用普通物理引擎进行试验,有情况的话后续再看需不需要做改造。
另外 Box2D 作为 2D 物理引擎也有所局限性,后续或许还会研究下 3D 类型的物理引擎。