foreword
In 3D games, there is always a protagonist. We can make the hero of the game move towards the click by clicking on other places in the game.
Then when we want to realize a function of "click the ground, the character moves to the clicked place", what preconditions are needed, and how to achieve it? This article takes you step by step to realize the function of characters walking and moving, and at the same time changing the state.
1. Skeletal animation
Skeleton animation (also known as skeleton animation) is a computer animation technology that divides a three-dimensional model into two parts: the skin used to draw the model, and the skeleton used to control the action.
Generally, the protagonist in a 3D game, its running, walking and standing movements are all built-in skeletal animations of the model file.
Skeletal Animation Weights <br>Changing the weights of skeletal animations can make the transition between animations more natural. For example, in the physical test, when you reach the end point, the speed will gradually slow down, the range of running movements will become smaller and smaller, then it will become walking, and finally stop.
Let's see an example of a two-action weight gradient:
In this example, from leisure to walking, the weight of the leisure animation decreases from 1 to 0, while the weight of the walking animation increases from 0 to 1. You can click 👉 in this website > Crossfading > from idle to walk to experience it.
In this 3D sandbox game, the state of the character changes, mainly after the mouse clicks on the ground, the character changes from the leisure state to the running state, and when the character reaches the destination, it becomes the leisure state again. Let's first look at how these state changes are implemented.
First, we need the designer to provide a model with skeletal animation, which has two skeletal animations, one for the idle (idle) state and one for the running (run) state.
1.1 Ideas
1.2 Animation initialization
First let's store the skeletal animation, animation name, and weight in an object,
idleAnimConfig = {
name: string;
anim: AnimationGroup;
weight: number;
}
So how do you know if you are walking? You need a flag of the current animation, set idle as the current animation when initializing
currentAnimConfig = idleAnimConfig
1.3 Animation weight change
As shown in the figure, when the character state changes, we need to increase the animation weight of the current state and decrease the animation weight of the other state (note that the weight value needs to be limited to [0, 1]). Let's look at the pseudocode, assuming deltaWeight
is positive
changeAnimWeight() {
// 当前动画 -> 递增
if (currentAnimConfig) {
setAnimationWeight(currentAnimConfig, deltaWeight)
}
// 其他动画 -> 递减,如站立动作切换到走路
if (currentAnimConfig !== idleAnimConfig) {
setAnimationWeight(idleAnimConfig, -deltaWeight)
}
// 其他动画 -> 递减,如走路动作切换到站立
if (currentAnimConfig !== runAnimConfig) {
setAnimationWeight(runAnimConfig, -deltaWeight)
}
}
Then when rendering, switch the state
onRender() {
if (准备到达目的地) {
setCurAnimation(runAnimConfig)
} else {
setCurAnimation(idleAnimConfig)
}
changeAnimWeight()
}
1.4 Missing animations
What if there is only one run animation in the animationGroup?
The answer is still the same, just set the skeletal animation of the idle animation to null
, like this:
idleAnimConfig = {
name: string;
anim: null;
weight: number;
}
This will allow reuse even if the character model with two animations is later replaced.
Animation state switching effect
2. Walking and moving
When we usually write animations, we will use rAF and recursively call the rendering function to achieve a frame-by-frame rendering animation. When the character walks on the flat ground, the frame-by-frame movement can also be used to achieve a displacement animation. For example, Babylon has encapsulated the event API of render , as long as we bind the rendering animation to the render event, it can be used.
Let's take a look at the specific ideas:
2.1 Mobile
As can be seen from the above ideas, we need to use several variables when moving:
- distance from the end point
- direction of movement
Then you need to get these variables when you click. The distance can be calculated by adding and subtracting the corresponding coordinates of the matrix, and the direction is the normal vector of the target position minus the initial position
directToPath() {
// 将人物的位置设为初始位置
initVec = this.player.position
// 计算初始位置与终点的距离
distance = Distance(targetVec, initVec)
// 将终点位置与初始位置相减
targetVec = targetVec.subtract(initVec)
// 使用法向量计算出与终点的朝向
direction = Normalize(targetVec)
player.lookAt(targetVec)
}
onClick() {
// ...
directToPath()
}
Displacement when rendering
onRender() {
if (distance > READY_ARRIVE) {
distance -= SPEED
// 人物朝 direction 方向移动 SPEED 距离
player.translate(direction, SPEED, Space.WORLD)
}
}
Displacement realization effect
2.2 Combining animation
When our movement is combined with the model's skeletal animation
Let's look at the pseudocode:
onRender() {
if (distance > READY_ARRIVE) {
distance -= SPEED
// 人物朝 direction 方向移动 SPEED 距离
player.translate(direction, SPEED, Space.WORLD)
setCurAnimation(runAnimConfig)
} else {
setCurAnimation(idleAnimConfig)
}
changeAnimWeight()
}
Displacement and state change realization effect
Three, character avoidance
3.1 Ideas
The character walking to avoid obstacles is actually from the starting point to the end point, adding an intermediate point to it. As shown
So we only need to record the path array from the current starting point to the ending point, and walk towards the Nth point of the array every time, we can turn. Next, we will refine the steps according to the ideas and pseudo-code.
(1) Record the path and initialize the current path index
path = getPath(targetVec)
prePathIdx = 0
(2) When reaching the current intermediate point, switch to the next intermediate point. When you reach the last one, stop
onRender() {
if (distance > READY_ARRIVE) {
// ...移动及动画权重切换...
} else {
switchPath()
// ...
}
// ...
}
switchPath() {
prePathIdx += 1
directToPath()
}
directToPath() {
const curPath = path[prePathIdx]
if (!curPath) return
// ...人物移动及转向...
}
3.2 Access to the actual obstacle avoidance algorithm
From 3.1 , it is known that the walking movement of characters needs to be connected to the obstacle avoidance algorithm, and the path planning array provided by the algorithm needs to be used. In practical applications, we only need to replace the getPath()
method in the pseudo code with the method of calculating the road by the algorithm.
3.2.1 RecastJSPlugin
Next, we use the Recast plug- in that comes with Babylon to explain how to access the obstacle avoidance algorithm.
method 1
In recast, the path can be obtained by computePath
:
const closestPoint = this.navigationPlugin.getClosestPoint(pickedPoint)
const path = this.navigationPlugin.computePath(
this._crowd.getAgentPosition(0),
closestPoint
)
Then use the idea of 3.1 to move through the path index switch.
Method 2
recast will first create a navmesh, then constrain them to this navmesh by adding agent
and a collection of these agent
called crowd
.
And recast comes with a mobile API -- agentGoto
, at this time, you don't need to calculate the distance and direction, and you don't need to manually switch the movement path. Let's see how it is done.
(1) Initialize the plugin and set up the Web Worker to get grid data to optimize performance
initNav() {
navigationPlugin = new RecastJSPlugin()
// 设置Web Worker,在里面获取网格数据
navigationPlugin.setWorkerURL(WORKER_URL)
// 创建导航mesh
navigationPlugin.createNavMesh([
ground,
...obstacleList, // 障碍物列表mesh
], NAV_MESH_CONFIG, (navMeshData) => {
navigationPlugin.buildFromNavmeshData(navMeshData)
}
this.navigationPlugin = navigationPlugin
}
(2) Initialize crowd (crowd: a collection of constraints in the navigation grid agent
)
initCrowd() {
this.crowd = this.navigationPlugin.createCrowd(1, MAX_AGENT_RADIUS, this.scene)
const transform = new TransformNode('playerTrans')
this.crowd.addAgent(this.player.position, AGENTS_CONFIG, transform)
}
(3) Use agentGoto
API to move when clicking, pickedPoint
is the 3D coordinates of the click point, since there is only one object in crowd, the index is 0
const closestPoint = this.navigationPlugin.getClosestPoint(pickedPoint)
this.crowd.agentGoto(0, closestPoint)
(4) Determine whether to stop, if not, change the character's orientation
So how to change the orientation of the character, we need the position of the next middle point, and let the character look at it.
So go back to the previous initialization and create a navigator
.
initCrowd() {
// ...
const navigator = MeshBuilder.CreateBox('navBall', {
size: 0.1,
height: 0.1,
}, this.scene)
navigator.isVisible = false
this.navigator = navigator
// ...
}
When rendering, whether the character stops can be judged by the current moving speed of agent
. And change the direction, by moving navigator
--- to the middle point of the next path
, let the character look at it.
onRender () {
// 第一个agent对象的移动速度
const velocity = this.crowd.getAgentVelocity(0)
// 移动人物到agent的位置
this.player.position = this.crowd.getAgentPosition(0)
// 将navigator的位置移到下一个点
this.crowd.getAgentNextTargetPathToRef(0, this.navigator.position)
if (velocity.length() > 0) {
this.player.lookAt(this.navigator.position)
// ...
} else {
// ...
}
// ...
}
4. Obstacle avoidance effect
Let's see the final effect
5. Problems encountered
In fact, the whole development process was not very smooth, and some problems encountered were summarized for your reference.
(1) When the Nian beast moves, it sometimes "cannot brake", resulting in repeated back and forth at the end of the beast can not stop;
This is because in this frame, due to the small acceleration of Nian Beast, it cannot reduce the speed to 0 in a short period of time. So we can only "go over" and then "walk back" until the speed drops to 0 and stop at the end point.
At this point, you only need to hack and set the maxAcceleration of the agent to be extremely large, so that it has a feeling of walking at a constant speed and stopping immediately.
export const AGENTS_CONFIG: IAgentParameters = {
maxAcceleration: 1000
// ...
}
(2) Dynamic addition and removal of obstacles
If the position of the obstacle changes after the scene is initialized, it is very expensive to destroy and create a navMesh at this time.
So by looking up the documentation, we saw that there is also an API for dynamically adding obstacles . Immediately adjusted the Playground in the document and found that it was available. But when we zoomed in on the obstacle, we went through the mold 👉 Take a look here .
So I asked this question on Babylon's forum, and I got a reply after 20 minutes. This speed 👍.
It turns out that the NavMeshParameters
ch
/ cs
/ tileSize
parameters need to be adjusted.
So if we want to implement obstacle avoidance and create a faster navMesh, what should we do? You can look at this article: 3D Sandbox Game Obstacle Avoidance, Stepping Pit and Realization Journey
Summarize
In this article, we explain the ideas and steps of realizing characters walking and moving and changing states in 3D sandbox games from three aspects: the introduction and use of skeletal animation, the movement of models and state changes, and the adaptation of path planning. After the newcomer finishes reading, they can get started with this feature faster.
Of course, the implementation method introduced in this article still has shortcomings, such as acceleration can be added to the movement, so that the movement and the movement speed can be matched more naturally.
If you have any other suitable suggestions, you are also welcome to leave a message and exchange.
References
- Skeletal animation - Wikipedia, the free encyclopedia
- Grouping Animations | Babylon.js Documentation
- Advanced Animation Methods | Babylon.js Documentation
- Vector3 | Babylon.js Documentation
- Crowd Navigation System | Babylon.js Documentation
- Web Workers API
Make crowd agent move at constant speed - Questions - Babylon.js
Welcome to the blog of Aotu Labs: aotu.io
Or pay attention to the AOTULabs official account (AOTULabs), and push articles from time to time.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。