更新于 

使用yuka控制物体运动

Yuka

游戏中存在很多可以交互的物体,
沿固定路径前进的NPC、主动追踪角色的怪物、巡逻搜查区域的敌人,
这些有物体一般统称Entity,
具有不同程度的AI能力,
yuka.js中提供了很多实现游戏ai的方法。

要实现实体沿固定路线移动,
需要做如下准备:

  • 创建一个实体Mesh(肉体)
  • 创建Mesh对应的Vehicle(灵魂)
  • 绑定Mesh和Vehicle
  • 创建路径path
  • 绑定路径和Vehicle
  • 创建实体管理器,管理实体
  • 随时间更新实体

首先,创建一个圆锥形的实体,
注意需要关闭实体矩阵的自动更新选项。

1
2
3
4
5
6
const vehicleGeo = new THREE.ConeGeometry(0.1, 0.8, 8)
vehicleGeo.rotateZ(Math.PI/2)
const vehicleMat = new THREE.MeshNormalMaterial()
const vehicleMesh = new THREE.Mesh(vehicleGeo, vehicleMat)
vehicleMesh.matrixAutoUpdate = false
scene.add(vehicleMesh)

创建Vehicle,将更新后的entity与mesh进行同步

1
2
3
4
5
6
const vehicle = new YUKA.Vehicle()
vehicle.maxSpeed = 2
vehicle.setRenderComponent(vehicleMesh, sync)
function sync(entity, renderComponent) {
renderComponent.matrix.copy(entity.worldMatrix)
}

创建循环路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const path = new YUKA.Path()
path.loop = true
path.add(new YUKA.Vector3(0,0,6))
path.add(new YUKA.Vector3(-4,0,4))
path.add(new YUKA.Vector3(-6,0,0))
path.add(new YUKA.Vector3(-4,0,-4))
path.add(new YUKA.Vector3(0,0,1))
path.add(new YUKA.Vector3(4,0,-4))
path.add(new YUKA.Vector3(6,0,0))
path.add(new YUKA.Vector3(4, 0, 4))

vehicleMesh.position.copy(path.current)

const points = path._waypoints.map(position => [position.x, position.y, position.z]).flat()
const lineGeo = new THREE.BufferGeometry()
lineGeo.setAttribute('position', new THREE.Float32BufferAttribute(points, 3))
const lineMat = new THREE.LineBasicMaterial()
const lineMesh = new THREE.LineLoop(lineGeo, lineMat)
scene.add(lineMesh)

const followPathBehovior = new YUKA.FollowPathBehavior(path, 0.5)
vehicle.steering.add(followPathBehovior)

const onePathBehavior = new YUKA.OnPathBehavior(path)
onePathBehavior.radius = 0
vehicle.steering.add(onePathBehavior)

添加实体管理器

1
2
3
4
5
6
7
8
9
const entityManager = new YUKA.EntityManager()
entityManager.add(vehicle)

const clock = new YUKA.Time()
function animate() {
let delta = clock.update().getDelta()
entityManager.update(delta)
renderer.render(scene, camera);
}

YUKA控制GLTF模型

gltf模型加载后,由于将matrixAutoUpdate关闭,无法对modelMatrix进行修改,
因此不能通过model.scale.set()的方式进行缩放,
需要通过YUKA.Vehicle提供的scale属性进行缩放

1
2
3
4
5
6
7
8
9
10
11
12
13
gltfLoader.load('/assets/Debris_BrokenCar.gltf', gltf => {
let model = gltf.scene
model.matrixAutoUpdate = false
scene.add(model)
vehicle.scale = new THREE.Vector3(0.5,0.5,0.5)
vehicle.setRenderComponent(model, sync)
})
const vehicle = new YUKA.Vehicle()
vehicle.maxSpeed = 3
function sync(entity, renderComponent) {
renderComponent.matrix.copy(entity.worldMatrix)
}

物体寻路 SeekBehavior

使用yuka做出A物体寻找B物体的效果,
需要使用到SeekBehavior
需要指定目标物体的坐标,
将SeekBehavior添加到动作主体中:

创建seek主体

1
2
3
4
const vehicle = new YUKA.Vehicle()
vehicle.maxSpeed = 3
vehicle.setRenderComponent(vehicleMesh, sync)
vehicle.position.set(-6,0,2)

创建target实体

1
2
const target = new YUKA.GameEntity()
target.setRenderComponent(targetMesh, sync)

添加追踪行为

1
2
const seekBehavior = new YUKA.SeekBehavior(target.position)
vehicle.steering.add(seekBehavior)

添加实体管理器并逐帧更新

1
2
3
4
5
6
7
8
9
10
const entityManager = new YUKA.EntityManager()
entityManager.add(vehicle)
entityManager.add(target)

const time = new YUKA.Time()
function animate() {
let delta = time.update().getDelta()
entityManager.update(delta)
renderer.render(scene, camera);
}

物体抵达 ArriveBehavior

ArriveBehaviorSeekBehavior类似,
Arrive在到达目的地后停止,
Seek依旧保持搜寻,
创建Arrive行为需要3个参数:

  • target 目的地
  • deceleration 减速率
  • tolerance 容差值
1
2
const arriveBehavior = new YUKA.ArriveBehavior(target.position,1,3)
vehicle.steering.add(arriveBehavior)

yuka控制物体移动到鼠标处

控制角色移动到鼠标点击处的游戏有很多,
使用yuka的ArriveBehavior,
结合raycaster获取鼠标坐标,就能实现这个功能。

基本思路就是:

  • 给追踪角色绑定Vehicle实体
  • 给被追踪物体绑定GameEntity实体
  • 绑定追踪关系,激活实体管理器
  • 创建辅助平面,监听鼠标点位
  • 点击屏幕,切换被追踪物坐标

这里给追踪的坦克模型添加了一个颠簸动画,
直接添加到vehicle或者target上会导致追踪物体上下翻滚的问题,
这里需要给vehicle外套一层group,
使用group控制上下颠簸效果,
追踪物位置依然由vehicle控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const gltfLoader = new GLTFLoader()
const group = new THREE.Group()
gltfLoader.load('/assets/Tank.gltf',gltf=>{
gltf.scene.getObjectByName('Cylinder010_1').material.color.set(0x5d3c1d)
gltf.scene.getObjectByName('Cylinder010_2').material.color.set(0xc3b598)
gltf.scene.matrixAutoUpdate = false
group.add(gltf.scene)
scene.add(group)
vehicle.setRenderComponent(gltf.scene, sync)
})

const time = new YUKA.Time()
function animate(t) {
let delta = time.update().getDelta()
entityManager.update(delta)
group.position.y = 0.05 * Math.sin(t/100)
renderer.render(scene, camera);
}

物体逃避 Flee Behavior

FleeBehavior

FleeBehavior的用法和其他行为相似,
需要2个参数:

  • target
  • panicDistance 规避范围
1
2
const fleeBehavior = new YUKA.FleeBehavior(target.position,5)
vehicle.steering.add(fleeBehavior)

障碍物规避 Orbstacle Avoidance

ObstacleAvoidanceBehavior

执行障碍物规避,需要知道一些关键条件:

  • 规避主体的碰撞体积
  • 被规避物体列表,以及它们对应的碰撞体积

计算碰撞体积需要配合Three.js中Geometry类型的computeBoundingSphere一起使用。

1
2
3
4
5
6
7
const geo = new BoxGeometry(1,1,1)
geo.computeBoundingSphere()
const mat = new THREE.MeshBasicMaterial()
const mesh = new THREE.Mesh(geo, mat)
const vehicle = new YUKA.Vehicle()
vehicle.setRenderComponent(mesh, sync)
vehicle.boundingRadius = geo.boundingSphere.radius

按照上面的计算方式,计算出规避物和障碍物的碰撞体积

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 碰撞物
const vehicleGeo = new THREE.ConeBufferGeometry(0.2,0.8,8)
vehicleGeo.computeBoundingSphere()
vehicleGeo.rotateX(Math.PI/2)
const vehicleMat = new THREE.MeshNormalMaterial()
const vehicleMesh = new THREE.Mesh(vehicleGeo, vehicleMat)
vehicleMesh.matrixAutoUpdate = false
scene.add(vehicleMesh)

const vehicle = new YUKA.Vehicle()
vehicle.setRenderComponent(vehicleMesh, sync)
vehicle.boundingRadius = vehicleGeo.boundingSphere.radius

// 障碍物
const obstacleGeo = new THREE.BoxGeometry(1,1,1)
obstacleGeo.computeBoundingSphere()
const obstacleMesh = new THREE.Mesh(
obstacleGeo,
new THREE.MeshStandardMaterial({color:0xffaa00})
)

const obstacleMesh1 = obstacleMesh.clone()
obstacleMesh1.position.set(-7,0,0)
scene.add(obstacleMesh1)
const obstacle1 = new YUKA.GameEntity()
obstacle1.position.copy(obstacleMesh1.position)
obstacle1.boundingRadius = obstacleGeo.boundingSphere.radius

// obstacle2...
// obstacle3...

最终绑定ObstacleAvoidanceBehavior

1
2
3
4
5
6
const obstacleAvoidanceBehavior = new YUKA.ObstacleAvoidanceBehavior([
obstacle1,
obstacle2,
obstacle3,
])
vehicle.steering.add(obstacleAvoidanceBehavior)

引入的gltf模型无法使用getBoundingSphere方式直接计算包围盒,
GPT说可以遍历gltf的每一个node计算包围盒(听上去像个馊主意)
试错试出一个boundingRadius之后,汽车绕开障碍物的轨迹非常僵硬,
需要增加运行轨迹的smooth程度:

1
2
const smoother = new YUKA.Smoother(30)
vehicle.smoother = smoother