前言

在 3D 游戏中,都会有一个主人公。我们可以通过点击游戏中的其他位置,使游戏主人公向点击处移动。

那当我们想要实现一个“点击地面,人物移动到点击处”的功能,需要什么前置条件,并且具体怎么实现呢?本文带大家一步步实现人物行走移动,同时进行状态改变的功能。

一、骨骼动画

骨骼动画(Skeleton animation 又称骨架动画,是一种计算机动画技术,它将三维模型分为两部分:用于绘制模型的蒙皮(Skin),以及用于控制动作的骨架。

一般在 3D 游戏中的主人公,它的跑步、走路、站立的动作,都是模型文件的自带骨骼动画。

骨骼动画权重

改变骨骼动画的权重,可以使得动画间的过渡更为自然。比如体测时,当你到达终点后,会逐渐减慢速度,跑步动作的幅度越来越小,然后变成走路,最后停止。

让我们看看一个俩动作权重渐变的例子:

这个例子中,从休闲变到走路,休闲动画的权重从1到0递减,同时走路动画的权重从0到1递增。可以的点击 这个网站中 > Crossfading > from idle to walk 体验一下。

在本次 3D 沙盒游戏中,人物状态改变,主要是鼠标点击地面后,人物从休闲状态转为跑步状态,当人物到达目的地后,又变为休闲状态。我们先来看看这些状态改变是如何实现的。

首先,我们需要设计师提供一个拥有骨骼动画的模型,它有两个骨骼动画,一个为休闲(idle)状态,一个为跑步(run)状态。

1.1 思路

1.2 动画初始化

先让我们将骨骼动画、动画名称、权重放到一个对象中存储起来,

idleAnimConfig = {
name: string;
anim: AnimationGroup;
weight: number;
}

那么如何判断是否正在行走呢?就需要一个当前动画的 flag,初始化时将 idle 设为当前动画

currentAnimConfig = idleAnimConfig

1.3 动画权重改变

如图,我们在人物状态改变时,需要将当前状态的动画权重递增,另一状态的动画权重递减(注意,权重值需要限制在[0, 1])。让我们看下伪代码,假设 deltaWeight 为正数

changeAnimWeight() {
// 当前动画 -> 递增
if (currentAnimConfig) {
setAnimationWeight(currentAnimConfig, deltaWeight)
}
// 其他动画 -> 递减,如站立动作切换到走路
if (currentAnimConfig !== idleAnimConfig) {
setAnimationWeight(idleAnimConfig, -deltaWeight)
}
// 其他动画 -> 递减,如走路动作切换到站立
if (currentAnimConfig !== runAnimConfig) {
setAnimationWeight(runAnimConfig, -deltaWeight)
}
}

然后在 render 的时候,进行状态切换

onRender() {
if (准备到达目的地) {
setCurAnimation(runAnimConfig)
} else {
setCurAnimation(idleAnimConfig)
}
changeAnimWeight()
}

1.4 缺少动画

如果 animationGroup 里只有一个 run 动画怎么办呢?

答案还是一样的,只要将 idle 动画的骨骼动画设为 null 即可,像这样:

idleAnimConfig = {
name: string;
anim: null;
weight: number;
}

这么做即使后来更换了具有两个动画的人物模型,也能复用。

动画状态切换实现效果

二、行走移动

当我们平常写动画时,会用到 rAF 并递归调用渲染函数,实现一个逐帧渲染动画。当人物行走在平地上时,也可以利用逐帧移动,来实现一个位移的动画。例如 Babylon 已经封装好了render 的事件 API,只要我们将渲染动画绑定 render 事件,就可以使用了。

让我们看看具体思路:

2.1 移动

由上面的思路可以看出,我们移动的时候需要用到几个变量:

  • 距终点的距离(distance)
  • 移动的方向(direction)

那么就需要在点击的时候,获取到这些变量。distance 可以利用矩阵对应坐标相加减计算,direction 就是目标位置减初始位置的法向量

directToPath() {
// 将人物的位置设为初始位置
initVec = this.player.position
// 计算初始位置与终点的距离
distance = Distance(targetVec, initVec)
// 将终点位置与初始位置相减
targetVec = targetVec.subtract(initVec)
// 使用法向量计算出与终点的朝向
direction = Normalize(targetVec)
player.lookAt(targetVec)
}
onClick() {
// ...
directToPath()
}

在 render 的时候进行位移

onRender() {
if (distance > READY_ARRIVE) {
distance -= SPEED
// 人物朝 direction 方向移动 SPEED 距离
player.translate(direction, SPEED, Space.WORLD)
}
}

位移实现效果

2.2 结合动画

当我们的移动结合模型的骨骼动画

让我们看看伪代码:

onRender() {
if (distance > READY_ARRIVE) {
distance -= SPEED
// 人物朝 direction 方向移动 SPEED 距离
player.translate(direction, SPEED, Space.WORLD)
setCurAnimation(runAnimConfig)
} else {
setCurAnimation(idleAnimConfig)
}
changeAnimWeight()
}

位移及状态变化实现效果

三、人物避障

3.1 思路

人物行走避障,实际上就是从起点到终点,在这之中添加了中间点。如图

所以我们只要记录下当前起点到终点这个路径数组,每次都朝数组的第N个点行走,就能做到转向。下面我们来根据思路及伪代码进行步骤细化。

(1) 记录路径和初始化当前的路径索引

path = getPath(targetVec)
prePathIdx = 0

(2) 当到达当前中间点时,切换到下一个中间点。当走到最后一个,则停止

onRender() {
if (distance > READY_ARRIVE) {
// ...移动及动画权重切换...
} else {
switchPath()
// ...
}
// ...
}
switchPath() {
prePathIdx += 1
directToPath()
}
directToPath() {
const curPath = path[prePathIdx]
if (!curPath) return
// ...人物移动及转向...
}

3.2 接入实际避障算法

3.1 得知,人物的行走移动要接入避障算法,需要利用到该算法提供的路径规划数组。实际应用中,我们只需要把,伪代码里的getPath()方法,换成算法计算道路的方法即可。

3.2.1 RecastJSPlugin

下面我们使用 Babylon 自带的 Recast 插件 ,来具体说明一下如何接入避障算法。

方法 1

在 recast 中,可以通过 computePath 获取路径:

const closestPoint = this.navigationPlugin.getClosestPoint(pickedPoint)
const path = this.navigationPlugin.computePath(
this._crowd.getAgentPosition(0),
closestPoint
)

然后利用 3.1 的思路,通过路径索引切换进行移动。

方法 2

recast 首先会创建一个导航网格,然后通过添加 agent 让它们约束在这个导航网格中,而这些 agent 的集合,称为 crowd

并且 recast 自带了移动的 API —— agentGoto,此时可以不需要再去计算距离和方向,并且也不需要手动切换移动路径,让我们看看具体是怎么做的。

(1) 初始化插件,并设置 Web Worker 来获取网格数据以优化性能

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) 初始化 crowd(crowd:约束在导航网格中 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) 点击时利用 agentGoto API进行移动,pickedPoint 为点击点的三维坐标,由于 crowd 里只有一个对象,所以索引是 0

const closestPoint = this.navigationPlugin.getClosestPoint(pickedPoint)
this.crowd.agentGoto(0, closestPoint)

(4) 判断是否停止,未停止则改变人物朝向

那么如何改变人物的朝向呢,我们需要下一个中间点的位置,让人物看向它即可。

所以回到之前初始化的地方,创建一个 navigator

initCrowd() {
// ...
const navigator = MeshBuilder.CreateBox('navBall', {
size: 0.1,
height: 0.1,
}, this.scene)
navigator.isVisible = false
this.navigator = navigator
// ...
}

在 render 的时候,人物是否停止,可以通过当前 agent 的移动速度来进行判断。而改变方向,则是通过将 navigator 移动到下一个 path 的中间点,让人物看向它。

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. 避障实现效果

让我们看看最后的效果

5. 遇到的问题

整个开发过程中,其实也不是非常顺利,总结了一些遇到的问题,可以给大家参考一下。

(1) 年兽移动时,有时会“无法刹车”,导致在终点时反复来回停不下来;

这是因为在这一帧里,由于年兽的加速度较小,无法使得短时间内将速度降为0。所以只能“走过头”再“走回来”直到速度降为0之后,停止在终点。

此时,只需要 hack 一下,将 agent 的 maxAcceleration 设为极大,让其有种匀速行走并立马停下的感觉。

export const AGENTS_CONFIG: IAgentParameters = {
maxAcceleration: 1000
// ...
}

(2) 障碍物的动态添加与移除

如果障碍物在该场景初始化后,位置发生了改变,此时再去销毁创建一次 navMesh 是很消耗性能的。

于是我们通过查找文档,看到还有动态添加障碍物的 API。再立马调了下文档中的Playground,发现是可以用的。但是当我们把障碍物放大了之后,穿模了 看看这里

于是在 Babylon 的论坛上提了这个问题,20分钟后就得到了 reply,这个速度。

原来是需要调整 NavMeshParametersch / cs / tileSize 参数,对项目做适配。

那如果想要自己实现避障,创建更快的navMesh,我们应该怎么做呢?可以看看这篇文章:3D 沙盒游戏之避障踩坑和实现之旅

总结

这篇文章,我们从骨骼动画的介绍及使用、模型的移动及状态改变、路径规划的适配三个方面,讲解了3D沙盒游戏中实现人物行走移动并进行状态改变的思路及步骤,希望新人阅读结束之后,能更快上手这个功能。

当然,本篇文章介绍的实现方式还仍有不足之处,比如移动可以加上加速度,让动作与移动速度匹配得更自然等。

如果还有什么合适的建议,也欢迎大家积极留言交流。

参考资料

  1. 骨骼动画 - 维基百科,自由的百科全书
  2. Grouping Animations | Babylon.js Documentation
  3. Advanced Animation Methods | Babylon.js Documentation
  4. Vector3 | Babylon.js Documentation
  5. Crowd Navigation System | Babylon.js Documentation
  6. Web Workers API
  7. Make crowd agent move at constant speed - Questions - Babylon.js

欢迎关注凹凸实验室博客:aotu.io

或者关注凹凸实验室公众号(AOTULabs),不定时推送文章:

3D 沙盒游戏之人物的点击行走移动的更多相关文章

  1. git 沙河游戏节点图, 自由沙盒模拟git, 各类交互git命令

    git学习练习总资源链接: https://try.github.io/ (练习已通,有document) 本沙盒游戏教学:https://learngitbranching.js.org/?demo ...

  2. 游戏开发设计模式之子类沙盒模式(unity3d 示例实现)

    积累提供所有操作(的实现)来定义子类的行为用一个最简单的例子来讲解这个模式玩家操纵的英雄也就是这个游戏的主角会有许多技能,我们想定义许多不同的技能,来让玩家使用.首 先我们定义一个skillBase类 ...

  3. <转>iOS应用程序内使用IAP/StoreKit付费、沙盒(SandBox)测试、创建测试账号流程!

    原文地址:http://blog.csdn.net/xiaominghimi/article/details/6937097 //——2012-12-11日更新   获取"产品付费数量等于0 ...

  4. NSFileManager(沙盒文件管理)数据持久化 <序列化与反序列化>

    iOS应用程序只能在为该改程序创建的文件中读取文件,不可以去其它地方访问,此区域被成为沙盒,所以所有的非代码文件都要保存在此,例如图像,图标,声音,映像,属性列表,文本文件等.       默认情况下 ...

  5. iOS开发——多线程篇——快速生成沙盒目录的路径,多图片下载的原理、SDWebImage框架的简单介绍

    一.快速生成沙盒目录的路径 沙盒目录的各个文件夹功能 - Documents - 需要保存由"应用程序本身"产生的文件或者数据,例如:游戏进度.涂鸦软件的绘图 - 目录中的文件会被 ...

  6. iOS开发——UI进阶篇(十一)应用沙盒,归档,解档,偏好设置,plist存储,NSData,自定义对象归档解档

    1.iOS应用数据存储的常用方式XML属性列表(plist)归档Preference(偏好设置)NSKeyedArchiver归档(NSCoding)SQLite3 Core Data 2.应用沙盒每 ...

  7. iOS学习之沙盒

    1.iOS沙盒 iOS应用程序只能在为该改程序创建的文件系统中读取文件,不可以去其它地方访问,此区域被成为沙盒,所以所有的非代码文件都要保存在此,例如图像,图标,声音,映像,属性列表,文本文件等. 1 ...

  8. IOS 沙盒机制 浅析

    IOS中的沙盒机制(SandBox)是一种安全体系,它规定了应用程序只能在为该应用创建的文件夹内读取文件,不可以访问其他地方的内容.所有的非代码文件都保存在这个地方,比如图片.声音.属性列表和文本文件 ...

  9. 【转】详解iOS应用程序内使用IAP/StoreKit付费、沙盒(SandBox)测试、创建测试账号流程

    http://blog.csdn.net/xiaominghimi/article/details/6937097 //——2012-12-11日更新   获取"产品付费数量等于0这个问题& ...

随机推荐

  1. Python 细聊从暴力(BF)字符串匹配算法到 KMP 算法之间的精妙变化

    1. 字符串匹配算法 所谓字符串匹配算法,简单地说就是在一个目标字符串中查找是否存在另一个模式字符串.如在字符串 "ABCDEFG" 中查找是否存在 "EF" ...

  2. BGP的五种报文六种状态

    BGP的五种报文 Open报文:用于协商BGP参数,包括版本号,AS号等信息.在两个路由器之间建立了TCP会话之后开始交换Open信息以确认是否能形成邻居关系,是TCP建立后发送的第一个信息,类似OS ...

  3. 数据结构 - 顺序表 C++ 实现

    顺序表 此处实现的顺序表为**第一个位置为 data[0] **的顺序表 顺序表的定义为 const int MAX = 50; typedef int ElemType; typedef struc ...

  4. JDK,JRE,JVM的作用及关系

    1.作用 JVM:Java虚拟机,保证Java语言跨平台 JRE:Java程序的运行环境 JDK:Java程序的开发环境 2.关系 JRE:JVM+类库 JDK:JRE+工具

  5. Idea集成CSSO插件压缩css文件

    首先需要本地已安装node环境,并且csso-cli已通过npm安装到本地目录,只要能找到就行. 1. 打开Settings配置,确认图中的 File Watchers 插件是否已存在,如果不存在,去 ...

  6. 为什么 String 在 Java 中是不可变的?

    我最喜欢的 Java 面试问题,很棘手,但同时也非常有用.一些面试者也常问这个问题,为什么 String 在 Java 中是 final 的.字符串在 Java 中是不可变的,因为 String 对象 ...

  7. Kafka 分区数可以增加或减少吗?为什么?

    我们可以使用 bin/kafka-topics.sh 命令对 Kafka 增加 Kafka 的分区数据,但是 Kafka 不支持减少分区数. Kafka 分区数据不支持减少是由很多原因的,比如减少的分 ...

  8. kafka partiton迁移方法与原理

    在kafka中增加新的节点后,数据是不会自动迁移到新的节点上的,需要我们手动将数据迁移(或者成为打散)到新的节点上 1 迁移方法 kafka为我们提供了用于数据迁移的脚本.我们可以用这些脚本完成数据的 ...

  9. redis主从复制与哨兵高可用

    redis主从复制 话不多说,直接看案例: 环境准备, 主从规划 主节点:6380 从节点:6381.6382 运行3个redis数据库,达到 1主 2从的配置 #主库 6379.conf port ...

  10. 从零开始开发一款H5小游戏(二) 创造游戏世界,启动发条

    本系列文章对应游戏代码已开源 Sinuous game 上一节介绍了canvas的基础用法,了解了游戏开发所要用到的API.这篇文章开始,我将介绍怎么运用这些API来完成各种各样的游戏效果.这个过程更 ...