前言

主角与NPC之间的交互,是虚拟世界中为NPC赋予灵性的重要环节。在传统RPG游戏中,往往通过对话、剧情演出等方式塑造NPC的人物形象,而在动作游戏中,NPC的动作表现受环境的反馈而对应变化也是其真实性的表现。

本文的主要内容是根据《原神》游戏内的实机表现,来反推其NPC动作系统中人物碰撞动作、人物受惊动作的实现机制。

从人物碰撞动作说起:

碰撞检测

首先,根据我对角色技能动作/表现动作的观察,原神的动作实现机制应该是网游中比较常见的Code Driven Locomotion,只是IK做的比较好,另外估计还自研了Root Motion Extractor将动画数据提取并应用到角色身上。这一方案下,对于角色位置信息的维护应该都是基于模型中心点来判断,那么关于角色碰撞大概率就是直接以空间距离来进行判定。

经测试,

案例1:从墙上往NPC头上跳/飞行时,如果按住方向轮盘触发输入则会触发碰撞动画,否则不会触发碰撞动画

案例2:当NPC主动朝你的方向走来并进入碰撞距离时,不会触发碰撞动画

案例3:当角色处于碰撞范围内时,朝任意方向发起跑步指令不会触发碰撞动画

案例4:以行走状态进行位移任何情况下不会触发碰撞动画

案例5: 处于特殊动作状态的NPC不受碰撞行为影响(如剧情任务中,蒙德城中受惊的NPC)

结合上面的测试案例,我们可以大胆推断如下几点: - 以一个巡逻士兵为例,Idle状态下无交互一定时间后进入Patrolling(巡逻)状态,这两个状态下如果受到碰撞进入RunInto(被碰撞)状态,如果被惊吓进入Threatened(被惊吓)状态,RunInto与Threatened状态下无交互一段时间后返回Idle状态或Patrolling状态。

写个伪代码的行为树实现:

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
enum STATUS = {
Idle,
Patrolling,
RunInto,
Threatened
}

status = STATUS.Idle
Loop:
//RunInto和Threatened两种状态下不可交互
If status == STATUS.Idle or STATUS.Patrolling:
If RunInto():
status = STATUS.RunInto
PlayAnim(Anim.RunInto, playerPosition)
WaitForSeconds(3)
status = STATUS.Idle
Elif Threatened():
PlayAnim(Anim.Threatened, playerPosition)
status = STATUS.Threatened
WaitForSeconds(3)
status = STATUS.Idle

If status == STATUS.Idle:
If AsyncWaitForSeconds(someTime):
status = STATUS.Patrolling
Patrol()

  • 当角色首次进入碰撞的范围时,若满足条件(角色处于奔跑状态),根据对方的位置改变自己的朝向并播放对应的动画(原神中有前向被撞和后向被撞两种动画)。伪代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 表示玩家是否首次进入碰撞区域
self.isPlayerNearBy = false
bool RunInto():
If Distance(self, player) < self.model.radius+0.1:
if self.isPlayerNearBy == false:
self.isPlayerNearBy == true
//当前左轮盘Input输入量大于某个阈值时才会触发碰撞动画
if player.STATUS = Running:
return true
Elif self.isPlayerNearBy == true:
self.isPlayerNearBy = false
return false

Funtion PlayAnim(Anim.RunInto, playerPosition):
//如果碰撞点在NPC的面前180度范围内(position是Vector2类型,dot是向量內积)
if dot((playerPosition - self.position), self.forwardDirection) > 0:
//面向主角
self.FaceTo(playerPosition)
AnimationClip("RunIntoFace").Play()
else:
//背向主角
self.FaceTo(2*self.position-playerPosition)
AnimationClip("RunIntoBack").Play()
  • 受惊吓的逻辑和被碰撞有些类似,条件是:

    1. 主角距NPC距离在X米内(目测3米)
    2. 主角面朝NPC(主角的前向180度角内)
    3. 主角进行攻击