HarmonyOS开发:游戏物理引擎——碰撞检测与物理模拟
HarmonyOS开发:游戏物理引擎——碰撞检测与物理模拟
📌 核心要点:物理引擎让游戏从"看起来对"变成"感觉上对",碰撞检测解决"撞没撞",物理模拟解决"撞了之后怎么动",性能和精度的平衡是核心矛盾。
背景与动机
你玩过一个叫"愤怒的小鸟"的游戏吗?小鸟飞出去的弧线、碰到木块后的散落、猪被砸中的翻滚——这些全都是物理引擎算出来的。
没有物理引擎的游戏,角色穿墙而过、子弹穿透敌人、箱子飘在空中。看起来就不对劲,玩起来更不对劲。
但物理引擎这东西,自己写吧,数学公式一堆;用现成的吧,鸿蒙上能用的物理库又不多。怎么办?
答案是:简单场景自己写,复杂场景用库。2D平台跳跃、弹幕射击这种,AABB碰撞+简单运动学就够了。3D射击、赛车、物理益智这种,得上刚体动力学。
这篇就帮你把碰撞检测和物理模拟的原理和实现搞清楚,让你知道什么时候该用什么方案。
核心原理
碰撞检测的两阶段策略
碰撞检测不是"两个对象碰没碰"这么简单。当场景里有几百个对象时,两两检查的复杂度是O(n²),性能根本扛不住。
所以碰撞检测分两个阶段:
graph LR
A[所有游戏对象] --> B[宽阶段 Broad Phase]
B -->|可能碰撞的对| C[窄阶段 Narrow Phase]
C -->|确认碰撞的对| D[碰撞响应]
subgraph 宽阶段算法
B1[空间网格]
B2[四叉树/八叉树]
B3[排序扫描]
end
subgraph 窄阶段算法
C1[AABB精确检测]
C2[圆形/球体检测]
C3[分离轴SAT]
C4[GJK算法]
end
B -.-> B1
B -.-> B2
B -.-> B3
C -.-> C1
C -.-> C2
C -.-> C3
C -.-> C4
classDef phaseStyle fill:#E74C3C,stroke:#C0392B,color:#fff,font-weight:bold
classDef broadStyle fill:#F39C12,stroke:#E67E22,color:#fff
classDef narrowStyle fill:#27AE60,stroke:#229954,color:#fff
classDef resultStyle fill:#3498DB,stroke:#2980B9,color:#fff
class A,B,C,D phaseStyle
class B1,B2,B3 broadStyle
class C1,C2,C3,C4 narrowStyle
宽阶段负责快速筛选"可能碰撞"的对象对,窄阶段负责精确判断"到底碰没碰"。两阶段配合,性能提升一个数量级。
刚体物理模拟
碰撞检测只告诉你"撞了",物理模拟告诉你"撞了之后怎么动"。
刚体动力学核心就三个量:位置、速度、加速度。加上牛顿第二定律F=ma,再加上碰撞后的动量守恒,就能模拟出真实的物理效果。
| 物理量 | 2D | 3D | 说明 |
|---|---|---|---|
| 位置 | x, y | x, y, z | 物体在哪 |
| 速度 | vx, vy | vx, vy, vz | 物体往哪走 |
| 加速度 | ax, ay | ax, ay, az | 速度怎么变 |
| 旋转 | θ | qx, qy, qz, qw | 物体转了多少 |
| 质量 | m | m | 物体有多重 |
| 力 | Fx, Fy | Fx, Fy, Fz | 外力作用 |
2D物理相对简单,旋转用角度就行。3D物理必须用四元数,不然万向节锁(Gimbal Lock)会让你抓狂。
性能与精度的平衡
物理模拟有个经典矛盾:算得越精确,性能消耗越大。
- 固定时间步长(Fixed Timestep):精度稳定,但帧率低时物理会变慢
- 可变时间步长(Variable Timestep):帧率自适应,但大deltaTime会导致物理不稳定
- 半隐式欧拉(Semi-implicit Euler):精度和性能的折中方案
- 韦尔莱积分(Verlet Integration):约束求解更稳定,适合布料和绳索
游戏开发最常用的是固定时间步长+半隐式欧拉。物理用固定步长算,渲染用可变步长画,两者解耦。
代码实战
基础用法:2D碰撞检测系统
先搞定2D碰撞检测——游戏开发中最常用的部分。
// Collision2D.ets - 2D碰撞检测
// 2D向量
class Vector2 {
x: number = 0
y: number = 0
constructor(x: number = 0, y: number = 0) {
this.x = x
this.y = y
}
add(v: Vector2): Vector2 { return new Vector2(this.x + v.x, this.y + v.y) }
sub(v: Vector2): Vector2 { return new Vector2(this.x - v.x, this.y - v.y) }
scale(s: number): Vector2 { return new Vector2(this.x * s, this.y * s) }
dot(v: Vector2): number { return this.x * v.x + this.y * v.y }
length(): number { return Math.sqrt(this.x * this.x + this.y * this.y) }
normalize(): Vector2 {
const len = this.length()
return len > 0 ? new Vector2(this.x / len, this.y / len) : new Vector2()
}
}
// AABB包围盒
class AABB {
minX: number = 0
minY: number = 0
maxX: number = 0
maxY: number = 0
constructor(minX: number, minY: number, maxX: number, maxY: number) {
this.minX = minX
this.minY = minY
this.maxX = maxX
this.maxY = maxY
}
// 中心点
get center(): Vector2 {
return new Vector2(
(this.minX + this.maxX) / 2,
(this.minY + this.maxY) / 2
)
}
// 半宽半高
get halfWidth(): number { return (this.maxX - this.minX) / 2 }
get halfHeight(): number { return (this.maxY - this.minY) / 2 }
// 是否包含点
contains(point: Vector2): boolean {
return point.x >= this.minX && point.x <= this.maxX &&
point.y >= this.minY && point.y <= this.maxY
}
// 是否与另一个AABB相交
intersects(other: AABB): boolean {
return this.minX < other.maxX && this.maxX > other.minX &&
this.minY < other.maxY && this.maxY > other.minY
}
}
// 圆形碰撞体
class Circle {
center: Vector2 = new Vector2()
radius: number = 0
constructor(cx: number, cy: number, r: number) {
this.center = new Vector2(cx, cy)
this.radius = r
}
}
// 碰撞检测结果
interface CollisionResult {
collided: boolean
normal: Vector2 // 碰撞法线
depth: number // 穿透深度
point: Vector2 // 碰撞点
}
// 2D碰撞检测器
class CollisionDetector2D {
// AABB vs AABB
static aabbVsAabb(a: AABB, b: AABB): CollisionResult {
if (!a.intersects(b)) {
return { collided: false, normal: new Vector2(), depth: 0, point: new Vector2() }
}
// 计算穿透深度和法线
const dx = b.center.x - a.center.x
const dy = b.center.y - a.center.y
const overlapX = a.halfWidth + b.halfWidth - Math.abs(dx)
const overlapY = a.halfHeight + b.halfHeight - Math.abs(dy)
// 取最小穿透轴
if (overlapX < overlapY) {
const normal = new Vector2(dx > 0 ? 1 : -1, 0)
return { collided: true, normal, depth: overlapX, point: a.center.add(normal.scale(a.halfWidth)) }
} else {
const normal = new Vector2(0, dy > 0 ? 1 : -1)
return { collided: true, normal, depth: overlapY, point: a.center.add(normal.scale(a.halfHeight)) }
}
}
// 圆 vs 圆
static circleVsCircle(a: Circle, b: Circle): CollisionResult {
const diff = b.center.sub(a.center)
const dist = diff.length()
const sumRadius = a.radius + b.radius
if (dist >= sumRadius) {
return { collided: false, normal: new Vector2(), depth: 0, point: new Vector2() }
}
const normal = dist > 0 ? diff.normalize() : new Vector2(1, 0)
const depth = sumRadius - dist
const point = a.center.add(normal.scale(a.radius))
return { collided: true, normal, depth, point }
}
// AABB vs 圆
static aabbVsCircle(box: AABB, circle: Circle): CollisionResult {
// 找到AABB上离圆心最近的点
const closestX = Math.max(box.minX, Math.min(circle.center.x, box.maxX))
const closestY = Math.max(box.minY, Math.min(circle.center.y, box.maxY))
const closest = new Vector2(closestX, closestY)
const diff = circle.center.sub(closest)
const dist = diff.length()
if (dist >= circle.radius) {
return { collided: false, normal: new Vector2(), depth: 0, point: new Vector2() }
}
const normal = dist > 0 ? diff.normalize() : new Vector2(0, -1)
const depth = circle.radius - dist
return { collided: true, normal, depth, point: closest }
}
}
进阶用法:刚体物理模拟
碰撞检测完了,接下来处理碰撞后的物理响应。
// RigidBody2D.ets - 2D刚体物理
// 刚体类型
enum BodyType {
STATIC, // 静态物体(墙壁、地面)
DYNAMIC, // 动态物体(角色、子弹)
KINEMATIC // 运动学物体(平台、电梯)
}
// 2D刚体
class RigidBody2D {
// 基本属性
position: Vector2 = new Vector2()
velocity: Vector2 = new Vector2()
acceleration: Vector2 = new Vector2()
force: Vector2 = new Vector2() // 累积力
impulse: Vector2 = new Vector2() // 冲量
// 物理属性
mass: number = 1.0
invMass: number = 1.0 // 质量的倒数,静态物体为0
restitution: number = 0.5 // 弹性系数(0=完全非弹性, 1=完全弹性)
friction: number = 0.3 // 摩擦系数
drag: number = 0.01 // 空气阻力
// 旋转
angle: number = 0
angularVelocity: number = 0
// 碰撞体
type: BodyType = BodyType.DYNAMIC
aabb: AABB = new AABB(0, 0, 0, 0)
circle?: Circle
// 碰撞回调
onCollision?: (other: RigidBody2D, result: CollisionResult) => void
constructor(type: BodyType = BodyType.DYNAMIC) {
this.type = type
if (type === BodyType.STATIC) {
this.invMass = 0
}
}
// 设置质量
setMass(m: number): void {
if (m <= 0 && this.type !== BodyType.STATIC) {
console.warn('质量必须大于0')
return
}
this.mass = m
this.invMass = this.type === BodyType.STATIC ? 0 : 1 / m
}
// 施加力
applyForce(f: Vector2): void {
this.force = this.force.add(f)
}
// 施加冲量
applyImpulse(imp: Vector2): void {
this.impulse = this.impulse.add(imp)
}
// 更新物理状态(半隐式欧拉积分)
integrate(dt: number): void {
if (this.type === BodyType.STATIC) return
// 加速度 = 力 / 质量
this.acceleration = new Vector2(
this.force.x * this.invMass,
this.force.y * this.invMass
)
// 速度 += 加速度 * dt + 冲量/质量
this.velocity = new Vector2(
this.velocity.x + this.acceleration.x * dt + this.impulse.x * this.invMass,
this.velocity.y + this.acceleration.y * dt + this.impulse.y * this.invMass
)
// 空气阻力
this.velocity = new Vector2(
this.velocity.x * (1 - this.drag),
this.velocity.y * (1 - this.drag)
)
// 位置 += 速度 * dt
this.position = new Vector2(
this.position.x + this.velocity.x * dt,
this.position.y + this.velocity.y * dt
)
// 旋转
this.angle += this.angularVelocity * dt
// 清除力和冲量
this.force = new Vector2()
this.impulse = new Vector2()
}
// 更新AABB
updateAABB(): void {
if (this.circle) {
this.aabb = new AABB(
this.position.x - this.circle.radius,
this.position.y - this.circle.radius,
this.position.x + this.circle.radius,
this.position.y + this.circle.radius
)
}
}
}
// 物理世界
class PhysicsWorld2D {
private bodies: RigidBody2D[] = []
private gravity: Vector2 = new Vector2(0, 400) // 重力加速度
private fixedDeltaTime: number = 1 / 60 // 固定物理步长
private accumulator: number = 0 // 时间累积器
// 添加刚体
addBody(body: RigidBody2D): void {
body.updateAABB()
this.bodies.push(body)
}
// 移除刚体
removeBody(body: RigidBody2D): void {
const idx = this.bodies.indexOf(body)
if (idx > -1) this.bodies.splice(idx, 1)
}
// 更新物理世界
step(dt: number): void {
// 固定时间步长累积
this.accumulator += dt
// 防止螺旋死亡(spiral of death)
if (this.accumulator > 0.1) {
this.accumulator = 0.1
}
// 用固定步长更新物理
while (this.accumulator >= this.fixedDeltaTime) {
this.fixedStep(this.fixedDeltaTime)
this.accumulator -= this.fixedDeltaTime
}
}
// 固定步长更新
private fixedStep(dt: number): void {
// 施加重力
for (const body of this.bodies) {
if (body.type === BodyType.DYNAMIC) {
body.applyForce(this.gravity.scale(body.mass))
}
}
// 积分更新
for (const body of this.bodies) {
body.integrate(dt)
body.updateAABB()
}
// 碰撞检测与响应
this.resolveCollisions()
}
// 碰撞检测与响应
private resolveCollisions(): void {
for (let i = 0; i < this.bodies.length; i++) {
for (let j = i + 1; j < this.bodies.length; j++) {
const a = this.bodies[i]
const b = this.bodies[j]
// 静态物体之间不检测
if (a.type === BodyType.STATIC && b.type === BodyType.STATIC) continue
// 宽阶段:AABB粗筛
if (!a.aabb.intersects(b.aabb)) continue
// 窄阶段:精确检测
const result = CollisionDetector2D.aabbVsAabb(a.aabb, b.aabb)
if (!result.collided) continue
// 碰撞响应
this.resolveCollision(a, b, result)
// 触发回调
if (a.onCollision) a.onCollision(b, result)
if (b.onCollision) b.onCollision(a, {
...result,
normal: result.normal.scale(-1) // 反转法线
})
}
}
}
// 碰撞响应
private resolveCollision(a: RigidBody2D, b: RigidBody2D, result: CollisionResult): void {
const { normal, depth } = result
// 位置修正:把重叠的物体推开
const totalInvMass = a.invMass + b.invMass
if (totalInvMass === 0) return
const correction = normal.scale(depth / totalInvMass * 0.8) // 80%修正
a.position = a.position.sub(correction.scale(a.invMass))
b.position = b.position.add(correction.scale(b.invMass))
// 速度响应:基于弹性系数
const relVel = b.velocity.sub(a.velocity)
const velAlongNormal = relVel.dot(normal)
// 物体正在分离,不处理
if (velAlongNormal > 0) return
// 计算弹性系数(取较小值)
const e = Math.min(a.restitution, b.restitution)
// 计算冲量标量
let j = -(1 + e) * velAlongNormal / totalInvMass
// 施加冲量
const impulse = normal.scale(j)
a.velocity = a.velocity.sub(impulse.scale(a.invMass))
b.velocity = b.velocity.add(impulse.scale(b.invMass))
// 摩擦力
const tangent = relVel.sub(normal.scale(velAlongNormal)).normalize()
const velAlongTangent = relVel.dot(tangent)
let jt = -velAlongTangent / totalInvMass
// 库仑摩擦定律
const mu = Math.sqrt(a.friction * b.friction)
if (Math.abs(jt) < j * mu) {
// 静摩擦
} else {
// 动摩擦
jt = -j * mu * Math.sign(jt)
}
const frictionImpulse = tangent.scale(jt)
a.velocity = a.velocity.sub(frictionImpulse.scale(a.invMass))
b.velocity = b.velocity.add(frictionImpulse.scale(b.invMass))
}
getBodies(): RigidBody2D[] {
return this.bodies
}
}
完整示例:物理弹球游戏
把碰撞检测和物理模拟串起来,做一个弹球游戏:
// PhysicsBallGame.ets - 物理弹球游戏
@Entry
@Component
struct PhysicsBallGame {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
private world: PhysicsWorld2D = new PhysicsWorld2D()
private balls: RigidBody2D[] = []
private walls: RigidBody2D[] = []
private canvasW: number = 360
private canvasH: number = 720
private running: boolean = false
private lastTime: number = 0
private timer: number = -1
private score: number = 0
aboutToAppear(): void {
this.setupWalls()
this.running = true
}
aboutToDisappear(): void {
this.running = false
if (this.timer !== -1) clearTimeout(this.timer)
}
// 设置边界墙壁
private setupWalls(): void {
const wallThickness = 20
// 左墙
const left = new RigidBody2D(BodyType.STATIC)
left.position = new Vector2(-wallThickness / 2, this.canvasH / 2)
left.aabb = new AABB(-wallThickness, 0, 0, this.canvasH)
this.world.addBody(left)
this.walls.push(left)
// 右墙
const right = new RigidBody2D(BodyType.STATIC)
right.position = new Vector2(this.canvasW + wallThickness / 2, this.canvasH / 2)
right.aabb = new AABB(this.canvasW, 0, this.canvasW + wallThickness, this.canvasH)
this.world.addBody(right)
this.walls.push(right)
// 地面
const ground = new RigidBody2D(BodyType.STATIC)
ground.position = new Vector2(this.canvasW / 2, this.canvasH + wallThickness / 2)
ground.aabb = new AABB(0, this.canvasH, this.canvasW, this.canvasH + wallThickness)
this.world.addBody(ground)
this.walls.push(ground)
// 天花板
const ceiling = new RigidBody2D(BodyType.STATIC)
ceiling.position = new Vector2(this.canvasW / 2, -wallThickness / 2)
ceiling.aabb = new AABB(0, -wallThickness, this.canvasW, 0)
this.world.addBody(ceiling)
this.walls.push(ceiling)
// 添加一些平台
this.addPlatform(80, 400, 120, 15)
this.addPlatform(200, 300, 120, 15)
this.addPlatform(140, 200, 100, 15)
}
// 添加平台
private addPlatform(x: number, y: number, w: number, h: number): void {
const platform = new RigidBody2D(BodyType.STATIC)
platform.position = new Vector2(x + w / 2, y + h / 2)
platform.aabb = new AABB(x, y, x + w, y + h)
this.world.addBody(platform)
this.walls.push(platform)
}
// 生成弹球
private spawnBall(x: number, y: number): void {
const ball = new RigidBody2D(BodyType.DYNAMIC)
ball.position = new Vector2(x, y)
ball.restitution = 0.7 + Math.random() * 0.3
ball.friction = 0.2
ball.setMass(1)
ball.circle = new Circle(x, y, 12 + Math.random() * 8)
ball.aabb = new AABB(
x - ball.circle.radius, y - ball.circle.radius,
x + ball.circle.radius, y + ball.circle.radius
)
// 随机初始速度
ball.velocity = new Vector2(
(Math.random() - 0.5) * 200,
-100 - Math.random() * 200
)
// 碰撞回调
ball.onCollision = (_other, _result) => {
this.score += 1
}
this.world.addBody(ball)
this.balls.push(ball)
}
build() {
Column() {
Canvas(this.ctx)
.width('100%')
.height('100%')
.onReady(() => {
this.lastTime = Date.now()
this.gameLoop()
})
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
const touch = event.touches[0]
this.spawnBall(touch.x, touch.y)
}
})
}
.width('100%')
.height('100%')
}
private gameLoop(): void {
if (!this.running) return
const now = Date.now()
const dt = Math.min((now - this.lastTime) / 1000, 0.05)
this.lastTime = now
// 更新物理
this.world.step(dt)
// 更新球体AABB
for (const ball of this.balls) {
if (ball.circle) {
ball.circle.center = ball.position
ball.updateAABB()
}
}
// 清理掉出屏幕的球
this.balls = this.balls.filter(b => b.position.y < this.canvasH + 100)
this.render()
this.timer = setTimeout(() => this.gameLoop(), 16)
}
private render(): void {
const ctx = this.ctx
ctx.clearRect(0, 0, this.canvasW, this.canvasH)
// 背景
ctx.fillStyle = '#1a1a2e'
ctx.fillRect(0, 0, this.canvasW, this.canvasH)
// 绘制平台
ctx.fillStyle = '#16213e'
for (const wall of this.walls) {
const aabb = wall.aabb
ctx.fillRect(aabb.minX, aabb.minY, aabb.maxX - aabb.minX, aabb.maxY - aabb.minY)
}
// 绘制弹球
const colors = ['#e94560', '#0f3460', '#533483', '#e94560', '#00b4d8']
for (let i = 0; i < this.balls.length; i++) {
const ball = this.balls[i]
if (!ball.circle) continue
ctx.beginPath()
ctx.arc(ball.position.x, ball.position.y, ball.circle.radius, 0, Math.PI * 2)
ctx.fillStyle = colors[i % colors.length]
ctx.fill()
// 高光效果
ctx.beginPath()
ctx.arc(
ball.position.x - ball.circle.radius * 0.3,
ball.position.y - ball.circle.radius * 0.3,
ball.circle.radius * 0.25,
0, Math.PI * 2
)
ctx.fillStyle = 'rgba(255,255,255,0.4)'
ctx.fill()
}
// UI
ctx.fillStyle = '#ffffff'
ctx.font = '18px sans-serif'
ctx.fillText(`点击屏幕生成弹球`, 10, 30)
ctx.fillText(`碰撞次数: ${this.score}`, 10, 55)
ctx.fillText(`弹球数量: ${this.balls.length}`, 10, 80)
}
}
跑起来,点击屏幕就能生成弹球,弹球受重力影响下落,碰到平台和墙壁会弹开,碰到其他弹球也会弹开。这就是物理引擎的效果——不需要手动计算每个球的运动轨迹,物理引擎自动帮你算。
踩坑与注意事项
坑1:穿透问题(Tunneling)
物体速度太快时,一帧就穿过了薄墙。这叫穿透问题(Tunneling)。解决方案有两种:
- 连续碰撞检测(CCD):在两帧之间做射线检测,找到碰撞的精确时刻。计算量大,但精确。
- 增大墙壁厚度/降低速度:简单粗暴,但有效。大部分2D游戏用这招就够了。
坑2:物理步长与渲染步长不同步
物理用1/60秒的固定步长,渲染用可变步长。如果渲染帧率是30FPS,一个渲染帧里会跑两个物理步长。这没问题。但如果渲染帧率是120FPS,一个渲染帧里只跑半个物理步长——物理状态在两个渲染帧之间是插值出来的。你需要做状态插值,不然画面会抖。
坑3:弹性系数设置不当
两个弹性系数都是1.0的物体碰撞,能量不会损失,会一直弹。看起来不真实。大部分游戏里,弹性系数设0.3-0.6比较自然。
坑4:碰撞法线方向
碰撞法线方向决定了物体弹开的方向。法线算反了,物体会"吸"到一起而不是弹开。记住:法线总是从A指向B。
坑5:螺旋死亡
物理步长太长时,物体穿透更深,需要更多步长来修正,导致更多穿透……形成恶性循环。这就是"螺旋死亡"。解决方案:限制累积器的最大值,超过就截断。
HarmonyOS 6适配说明
HarmonyOS 6在物理引擎方面没有新增专门的物理API,但有几个间接利好:
-
计算性能提升:ArkTS引擎的数值计算性能提升约20%,物理模拟的帧时间更短,可以支持更复杂的物理场景。
-
Worker多线程:HarmonyOS 6增强了Worker的能力,支持SharedArrayBuffer。你可以把物理计算放到Worker线程,渲染留在主线程,两者并行执行。
// HarmonyOS 6 Worker物理计算示例
import worker from '@ohos.worker'
// 主线程
const physicsWorker = new worker.ThreadWorker('workers/PhysicsWorker.ets')
// 发送物理更新请求
physicsWorker.postMessage({
type: 'step',
deltaTime: dt,
bodies: this.serializeBodies()
})
// 接收物理计算结果
physicsWorker.onmessage = (e) => {
const result = e.data
this.deserializeBodies(result.bodies)
}
- SIMD支持:HarmonyOS 6的ArkTS引擎开始支持SIMD指令,向量运算性能大幅提升。碰撞检测中的大量向量运算可以直接受益。
总结
物理引擎让游戏从"看起来对"变成"感觉上对"。碰撞检测解决"撞没撞"的问题,物理模拟解决"撞了之后怎么动"的问题。
2D游戏用AABB+简单运动学就够;3D游戏需要球体/凸包碰撞+刚体动力学。碰撞检测用两阶段策略(宽阶段粗筛+窄阶段精检)保证性能。物理模拟用固定时间步长+半隐式欧拉保证稳定性。
性能和精度永远在博弈。你的任务不是追求物理上100%精确,而是让玩家"感觉"物理是对的。
| 评估维度 | 学习难度 | 使用频率 | 重要程度 |
|---|---|---|---|
| AABB碰撞检测 | ★★☆☆☆ | ★★★★★ | ★★★★★ |
| 圆形碰撞检测 | ★★☆☆☆ | ★★★★☆ | ★★★★☆ |
| 碰撞响应 | ★★★★☆ | ★★★★★ | ★★★★★ |
| 刚体物理模拟 | ★★★★★ | ★★★★☆ | ★★★★☆ |
| 固定时间步长 | ★★★☆☆ | ★★★★★ | ★★★★★ |
| 空间分区优化 | ★★★★☆ | ★★★☆☆ | ★★★★☆ |
| 穿透问题处理 | ★★★☆☆ | ★★★☆☆ | ★★★★☆ |
下一篇讲游戏音频——背景音乐和音效管理。画面再好看,没有声音的游戏就像默片,少了灵魂。
- 点赞
- 收藏
- 关注作者
评论(0)