Movement
Movement
Basic Movement
移动算法的结构图
2D
角色在2维空间移动,大多数3维游戏也可以看做二维的移动。

Statics
在处理角色的位置、转向这些数据使用的公式与算法叫做静态,因为这些数据不含有任何角色移动的数据。数据结构可以定义为:
class Static:
position: Vector
orientation: float
Kinematic
如果一个角色正在向一个方向移动,突然改变他的速度与方向,这看起来有点突兀。为了让这个运动更加的丝滑,不让角色加速太快。我们就需要一些算法去考虑角色当前的速度,使用合理的加速度去改变速度。
我们就要记录这个角色的速度与转向的速度(角速度),我们可以定义数据结构为:
class Kinematic:
position: Vector
orientation: float
velocity: Vector
rotation: float
Kinematic Movement
Seek
给定一个角色的静态数据以及目标的静态数据。来计算角色到目标的方向以及一个直线速度。大概实现如下:
class KinematicSeek:
character: Static
target: Static
maxSpeed: float
function getSteering() -> KinematicSteeringOutput:
result = new KinematicSteeringOutput()
# Get the direction to the target.
result.velocity = target.position - character.position
# The velocity is along this direction, at full speed.
result.velocity.normalize()
result.velocity *= maxSpeed
# Face in the direction we want to move.
character.orientation = newOrientation(
character.orientation,
result.velocity)
result.rotation = 0
return result
这个算法可以用于追击的一些情况。但是我们如果要让角色到一个点,然后停下来。我们可以添加一个以目标点为圆心的半径。当我们的角色到这个半径内时,就停止移动。当我们的设置半径不合理时,太小了,会使角色一直到不了目标点,还会看到角色的抖动。这里有几种解决方式:
- 固定时间到达目标点,需要设置一个最大速度阀值
- 扩大半径
class KinematicSeek:
character: Static
target: Static
maxSpeed: float
function getSteering() -> KinematicSteeringOutput:
result = new KinematicSteeringOutput()
# Get the direction to the target.
result.velocity = target.position - character.position
# Check if we’re within radius.
if result.velocity.length() < radius:
# Request no steering.
return null
# We need to move to our target, we’d like to
# get there in timeToTarget seconds.
result.velocity /= timeToTarget
# If this is too fast, clip it to the max speed.
if result.velocity.length() > maxSpeed:
result.velocity.normalize()
result.velocity *= maxSpeed
# Face in the direction we want to move.
character.orientation = newOrientation(
character.orientation,
result.velocity)
result.rotation = 0
return result
Wandering
Wandering(漫游)是一种让角色产生平滑随机游走效果的转向行为,其核心思想是:在角色前方一段固定距离处投射一个小圆(wander circle),目标点始终被约束在该圆的圆周上,每帧对目标点的角度施加一个小幅随机扰动(wander angle jitter),从而驱动角色连续、平滑地改变方向。
与简单地"每帧随机选一个新方向"相比,Wandering 的优势在于:随机量只作用于圆上的角度偏移,而非整体速度方向,因此方向变化是渐进的,视觉上不会产生抖动或突然转弯的不自然感。
class Wander:
character: Kinematic
# 投射到角色前方的距离
wanderOffset: float
# 投射圆的半径
wanderRadius: float
# 每帧最大角度扰动量(弧度)
wanderRate: float
# 当前目标点在圆上的角度(随帧累积)
wanderOrientation: float
maxAcceleration: float
function getSteering() -> SteeringOutput:
result = new SteeringOutput()
# 在当前朝向上随机扰动 wander 角度
wanderOrientation += randomBinomial() * wanderRate
# 目标朝向 = 角色朝向 + 本帧 wander 角度
targetOrientation = wanderOrientation + character.orientation
# 投射圆的圆心(角色前方 wanderOffset 处)
wanderCenter = character.position +
wanderOffset * asVector(character.orientation)
# 目标点 = 圆心 + wanderRadius * 目标朝向对应的单位向量
target = wanderCenter + wanderRadius * asVector(targetOrientation)
# 以最大加速度朝目标点施力
result.linear = maxAcceleration * (target - character.position).normalized()
result.angular = 0
return result
其中 randomBinomial() 返回 asVector(orientation) 将朝向角转换为单位向量。
Arrive
可以设置以目标点设置两个圈,一个大半径圈,当进入这个圈后,角色开始减速。小圈是判断角色是否已经到达。
class Arrive:
character: Kinematic
target: Kinematic
maxAcceleration: float
maxSpeed: float
# The radius for arriving at the target.
targetRadius: float
# The radius for beginning to slow down.
slowRadius: float
# The time over which to achieve target speed.
timeToTarget: float = 0.1
function getSteering() -> SteeringOutput:
result = new SteeringOutput()
# Get the direction to the target.
direction = target.position - character.position
distance = direction.length()
# Check if we are there, return no steering.
if distance < targetRadius:
return null
# If we are outside the slowRadius, then move at max speed.
if distance > slowRadius:
targetSpeed = maxSpeed
# Otherwise calculate a scaled speed.
else:
targetSpeed = maxSpeed * distance / slowRadius
# The target velocity combines speed and direction
targetVelocity = direction
targetVelocity.normalize()
targetVelocity *= targetSpeed
# Acceleration tries to get to the target velocity.
result.linear = targetVelocity - character.velocity
result.linear /= timeToTarget
# Check if the acceleration is too fast.
if result.linear.length() > maxAcceleration:
result.linear.normalize()
result.linear *= maxAcceleration
result.angular = 0
return result
在很多实现中并没有使用到大圈这种方式,因为在减速是会有震荡的可能性
ALIGN
对齐使角色的转向与目标的转向相匹配。
class Align:
character: Kinematic
target: Kinematic
maxAngularAcceleration: float
maxRotation: float
# The radius for arriving at the target.
targetRadius: float
# The radius for beginning to slow down.
slowRadius: float
# The time over which to achieve target speed.
timeToTarget: float = 0.1
function getSteering() -> SteeringOutput:
result = new SteeringOutput()
# Get the naive direction to the target.
rotation = target.orientation - character.orientation
# Map the result to the (-pi, pi) interval.
rotation = mapToRange(rotation)
rotationSize = abs(rotation)
# Check if we are there, return no steering.
if rotationSize < targetRadius:
return null
# If we are outside the slowRadius, then use maximum tation.
if rotationSize > slowRadius:
targetRotation = maxRotation
# Otherwise calculate a scaled rotation.
else:
targetRotation = maxRotation * rotationSize / slowRadius
# The final target rotation combines speed (already inthe
# variable) and direction.
targetRotation *= rotation / rotationSize
# Acceleration tries to get to the target rotation.
result.angular = targetRotation - character.rotation
result.angular /= timeToTarget
# Check if the acceleration is too great.
angularAcceleration = abs(result.angular)
if angularAcceleration > maxAngularAcceleration:
result.angular /= angularAcceleration
result.angular *= maxAngularAcceleration
result.linear = 0
return result
VELOCITY MATCHING
把角色的速度与目标的数据设置为一样
class VelocityMatch:
character: Kinematic
target: Kinematic
maxAcceleration: float
# The time over which to achieve target speed.
timeToTarget = 0.1
function getSteering() -> SteeringOutput:
result = new SteeringOutput()
# Acceleration tries to get to the target velocity.
result.linear = target.velocity - character.velocity
result.linear /= timeToTarget
# Check if the acceleration is too fast.
if result.linear.length() > maxAcceleration:
result.linear.normalize()
result.linear *= maxAcceleration
result.angular = 0
return result
FACE
这个行为使角色看向目标,他委托对齐行为但是先计算目标的方向。
class Face extends Align:
# Overrides the Align.target member.
target: Kinematic
# ... Other data is derived from the superclass ...
# Implemented as it was in Pursue.
function getSteering() -> SteeringOutput:
# 1. Calculate the target to delegate to align
Work out the direction to target.
direction = target.position - character.position
# Check for a zero direction, and make no change if so.
if direction.length() == 0:
return target
# 2. Delegate to align.
Align.target = explicitTarget
Align.target.orientation = atan2(-direction.x, direction.z)
return Align.getSteering()
LOOKING WHERE YOU`RE GOING
class LookWhereYoureGoing extends Align:
# No need for an overridden target member, we have
# no explicit target to set.
# ... Other data is derived from the superclass ...
function getSteering() -> SteeringOutput:
# 1. Calculate the target to delegate to align
# Check for a zero direction, and make no change if so.
velocity: Vector = character.velocity
if velocity.length() == 0:
return null
# Otherwise set the target based on the velocity.
target.orientation = atan2(-velocity.x, velocity.z)
# 2. Delegate to align.
return Align.getSteering()