- 完整代码
https://github.com/CheapMiao/Godot-ThirdPersonController
extends KinematicBody
# ---对象属性---
# 移动速度
export var moveSpeed : float = 10
# 跳跃速度
export var jumpAcceleration : float = 200
# 下落加速度
export var fallAcceleration : float = 9.8
# 线速度
var linearVelocity : Vector3 = Vector3.ZERO
# 鼠标灵敏度
export var mouseSensitivity : float = 0.05
# 鼠标最大移动速度
export var mouseMoveMaxSpeed : float = 10
# 最小俯仰角
export var cameraMinPitch : float = -45
# 最大俯仰角
export var cameraMaxPitch : float = 90
# 角色转身速度
export var playerRotSpeed : float = 0.2
# 角色在斜面上滑动的加速度
export var slipAcceleration : float = 1
# ---组件引用---
# Mesh
onready var meshes = $Meshes
# 弹簧臂
onready var springarm = $SpringArm
# 摄像机
onready var camera = $SpringArm/Camera
# ---控制缓存---
# 是否应该旋转弹簧臂
var shouldCameraMove : bool = false
# 物体坐标系二维平面上鼠标运动方向 向上向左为负 向下向右为正
var mouseMoveSpeed = Vector2(0,0)
# y 方向上的加速度
var yAcceleration = 0
# ---控制参数---
# y 方向加速度的缩放比例
# 为了让 fallAcceleration 保持 9.8 不变,符合常识
var yAccelerationScale : float = 10
# ---事件---
func _ready():
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func _unhandled_input(event) -> void:
# 如果获得”鼠标正在运动“事件
if event is InputEventMouseMotion:
# 如果得到鼠标相对于最后一帧的位移不是 0 而是 Vector2,说明鼠标相对于最后一帧移动了
if typeof(event.relative) == TYPE_VECTOR2:
# 应该旋转摄像机
shouldCameraMove = true
# 获得鼠标在一帧内的移动量
mouseMoveSpeed = event.relative
# 如果按退出键
if Input.is_action_just_released("ui_cancel"):
print("cancel")
# 在鼠标隐藏和固定之间切换
if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
else:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
# ---自定义函数---
# 玩家运动
func playerMove(deltaTime):
# ---水平方向---
# 控制缓存 移动方向
var direction = Vector3.ZERO
# 获取摄像机地前后左右方向
# 注意 xyz 坐标系的方向
if Input.is_action_pressed("move_right"):
direction += camera.get_global_transform().basis.x
if Input.is_action_pressed("move_left"):
direction -= camera.get_global_transform().basis.x
if Input.is_action_pressed("move_up"):
direction -= camera.get_global_transform().basis.z
if Input.is_action_pressed("move_down"):
direction += camera.get_global_transform().basis.z
# 水平移动方向单位化
if direction != Vector3.ZERO:
direction = direction.normalized()
# 水平线速度
linearVelocity = direction * moveSpeed
# ---竖直方向---
# 在地面上,判断是否跳跃
if is_on_floor():
# 在地面起跳,跳跃加速度
if Input.is_action_pressed("jump"):
yAcceleration = jumpAcceleration
# 在地面上没有起跳,那么向下的加速度为斜面滑动加速度
else:
yAcceleration = slipAcceleration
# 不在地面上,重力加速度
else:
yAcceleration -= fallAcceleration
# 应用 y 方向加速度
linearVelocity += Vector3.UP * yAcceleration / yAccelerationScale
# 角色移动
linearVelocity = move_and_slide(linearVelocity, Vector3.UP)
# 摄像机旋转
func cameraRotate(deltaTime):
# 如果需要旋转摄像机
if shouldCameraMove:
# 已经开始旋转摄像机
shouldCameraMove = false
# 旋转摄像机
camera.rotate_x(-lerp(0, mouseSensitivity, mouseMoveSpeed.y/mouseMoveMaxSpeed))
# 钳制
camera.rotation_degrees.x = clamp(camera.rotation_degrees.x,cameraMinPitch,cameraMaxPitch)
# 旋转弹簧臂
springarm.rotate_y(-lerp(0, mouseSensitivity, mouseMoveSpeed.x/mouseMoveMaxSpeed))
# 玩家模型旋转
func meshesRotate(deltaTime):
# meshes 前方向
var meshesForwardVector = meshes.get_global_transform().basis.z
# 弹簧臂 前方向 由于我弹簧臂摆放的设置,这个获得的前方向和期望的前方向是相反的
var springarmForwardVector = -springarm.get_global_transform().basis.z
# meshes 前方向 和 弹簧臂 前方向 之间的夹角
var angle = meshesForwardVector.angle_to(springarmForwardVector)
# 从 meshes 前方向 到 弹簧臂 前方向 的向量
var deltaVector = springarmForwardVector - meshesForwardVector
# rotate_x 增加的方向是逆时针方向
# 如果从 meshes 前方向 到 弹簧臂 前方向 是顺时针方向,就把 angle 设为负
if deltaVector.dot(meshes.get_global_transform().basis.x) < 0:
angle = -angle
# 应用角色转身速度
angle *= playerRotSpeed
# meshes 旋转
meshes.rotate_y(angle)
# ---虚函数实现---
# 固定帧率执行
func _physics_process(deltaTime):
playerMove(deltaTime)
cameraRotate(deltaTime)
meshesRotate(deltaTime)
布局:
- Debug 过程
一开始做的弹簧臂旋转
# 弹簧臂
onready var springArm = $SpringArm
func _unhandled_input(event):
if event is InputEventMouseMotion:
# 物体坐标系二维平面上鼠标运动方向 向上向左为负 向下向右为正
var mouseMoveLocalDir = event.speed.normalized()
# 物体坐标系二维平面上鼠标运动速度大小 视为以玩家为球心,弹簧臂为半径的球上的弧长
var mouseMoveArcLength = event.speed.length()
# 世界坐标系三维空间中鼠标运动方向
var mouseMoveWorldDir = to_global(Vector3(-mouseMoveLocalDir.x,-mouseMoveLocalDir.y,0))
# 世界坐标系三维空间中玩家前方向
var playerWorldForwardDir = get_global_transform().basis.z
# 世界坐标系三维空间中弹簧臂旋转轴 为三维空间中鼠标运动方向和玩家前方向的叉乘
var springArmRotAxis = mouseMoveWorldDir.cross(playerWorldForwardDir)
# 弹簧臂的旋转角 等于弧长除于半径
var mouseMoveAngle = mouseMoveArcLength/clamp(springArm.get_hit_length(),1,springArm.spring_length)
# 弹簧臂旋转
springArm.global_rotate(springArmRotAxis,mouseMoveAngle)
运行起来一团糟,然后我觉得可能是需要放到 _physics_process 中,所以改成了
# 鼠标灵敏度
var mouseSensitivity = 0.01
# ---组件引用---
# 弹簧臂
onready var springarm = $SpringArm
# ---控制缓存---
# 物体坐标系二维平面上鼠标运动方向 向上向左为负 向下向右为正
var mouseMoveLocalDir = Vector2(0,0)
# 物体坐标系二维平面上鼠标运动速度大小 视为以玩家为球心,弹簧臂为半径的球上的弧长
var mouseMoveArcLength = 0
# ---输入事件---
# 获取鼠标运动状态
func _unhandled_input(event):
if event is InputEventMouseMotion:
mouseMoveLocalDir = event.speed.normalized()
mouseMoveArcLength = event.speed.length()
else:
mouseMoveLocalDir = Vector2(0,0)
mouseMoveArcLength = 0
# ---自定义函数---
# 弹簧臂旋转
func springarmRotate(deltaTime):
# 世界坐标系三维空间中鼠标运动方向
var mouseMoveWorldDir = to_global(Vector3(-mouseMoveLocalDir.x,-mouseMoveLocalDir.y,0))
# 世界坐标系三维空间中玩家前方向
var playerWorldForwardDir = get_global_transform().basis.z
# 世界坐标系三维空间中弹簧臂旋转轴 为三维空间中鼠标运动方向和玩家前方向的叉乘 需要单位化
var springarmRotAxis = (mouseMoveWorldDir.cross(playerWorldForwardDir)).normalized()
# 弹簧臂的旋转角 等于弧长除于半径
var mouseMoveAngle = mouseMoveArcLength/clamp(springarm.get_hit_length(),1,springarm.spring_length)
# 弹簧臂旋转 要考虑物理时间间隔和鼠标灵敏度
springarm.global_rotate(springarmRotAxis,mouseMoveAngle*deltaTime*mouseSensitivity)
# ---虚函数实现---
# 固定帧率执行
func _physics_process(deltaTime):
springarmRotate(deltaTime)
print(mouseMoveArcLength)
运行起来,旋转方向是正确的,但是会出现频闪,还有鼠标静止时仍在旋转的问题。
这一看就是 _unhandled_input 中的 InputEventMouseMotion 对鼠标移动速度的获取出了问题。理想情况下,应该是鼠标移动时,_unhandled_input 识别到 InputEventMouseMotion,获得鼠标速度;鼠标不移动时,_unhandled_input 识别不到 InputEventMouseMotion,不获得鼠标速度
测试 _unhandled_input 获得 event 的机制
# ---输入事件---
var tmp = null
# 获取鼠标运动状态
func _unhandled_input(event):
tmp = event
# ---虚函数实现---
# 固定帧率执行
func _physics_process(deltaTime):
print(tmp)
结果是,只要把鼠标在窗口中滑动一下,然后鼠标不动,打印出来的 event 就只是最后一次 InputEventMouseMotion,也就是说,只有有 input 的时候,_unhandled_input 才工作
官方文档 http://godot.pro/doc/tutorials/inputs/inputevent.html 中解释了 inputevent 的传递流,它的意思就是,有 inputevnet,_unhandled_input 才可能工作。
综合:
① 有 input 的时候,_unhandled_input 才工作
② 有 inputevnet,_unhandled_input 才可能工作
可见,没有 input 就没有 inputevent
鼠标静止的时候,就是没有 input 的时候,所以鼠标静止不会给出一个 inputevent 放到 _unhandled_input 中,所以我的使用 _unhandled_input 获得鼠标移动速度的逻辑有问题,只能获取到最后一次记录的鼠标移动的速度。
……好吧,虽然这是可以理解的
之后又搜了一下,看到别人也问过这个问题 https://*.com/questions/62844337/godot-how-would-i-get-inputeventmousemotion-in-the-process-function,现在可以知道,我写的这个获取鼠标速度的错误逻辑就相当于 Input.get_last_mouse_speed()
因此我又把旋转脚本改为
# ---对象属性---
# 鼠标灵敏度
var mouseSensitivity = 0.01
# ---组件引用---
# 弹簧臂
onready var springarm = $SpringArm
# 弹簧臂旋转
func springarmRotate(deltaTime):
# 物体坐标系二维平面上鼠标运动方向 向上向左为负 向下向右为正
var mouseMoveLocalDir = Vector2(0,0)
# 物体坐标系二维平面上鼠标运动速度大小 视为以玩家为球心,弹簧臂为半径的球上的弧长
var mouseMoveArcLength = 0
# 如果鼠标正在移动,那么获得最后一次记录的鼠标移动速度
if Input.get_current_cursor_shape() == Input.CURSOR_MOVE:
print("mouse is moving")
mouseMoveLocalDir = Input.get_last_mouse_speed().normalized()
mouseMoveArcLength = Input.get_last_mouse_speed().length()
# 世界坐标系三维空间中鼠标运动方向
var mouseMoveWorldDir = to_global(Vector3(-mouseMoveLocalDir.x,-mouseMoveLocalDir.y,0))
# 世界坐标系三维空间中玩家前方向
var playerWorldForwardDir = get_global_transform().basis.z
# 世界坐标系三维空间中弹簧臂旋转轴 为三维空间中鼠标运动方向和玩家前方向的叉乘 需要单位化
var springarmRotAxis = (mouseMoveWorldDir.cross(playerWorldForwardDir)).normalized()
# 弹簧臂的旋转角 等于弧长除于半径
var mouseMoveAngle = mouseMoveArcLength/clamp(springarm.get_hit_length(),1,springarm.spring_length)
# 弹簧臂旋转 要考虑物理时间间隔和鼠标灵敏度
springarm.global_rotate(springarmRotAxis,mouseMoveAngle*deltaTime*mouseSensitivity)
# ---虚函数实现---
# 固定帧率执行
func _physics_process(deltaTime):
springarmRotate(deltaTime)
运行结果是根本不旋转,根本没进入 if Input.get_current_cursor_shape() == Input.CURSOR_MOVE
于是我在 _physics_process 中试了一下 print(Input.get_current_cursor_shape()),发现我不管怎么移动鼠标或者点击鼠标按键,它一直打印的是 0,即 CURSOR_ARROW
箭头模式……我也知道是箭头模式啊……思考……不知道他这个设计是为了什么,好反直觉
再看它的英文含义,有没有可能是我用错了?get_current_cursor_shape 和 get_mouse_mode,一个是 获得鼠标形状 一个是 获得鼠标状态,都一直获得 0,也就是说,一直是 MOUSE_MODE_VISIBLE 和 CURSOR_ARROW
虽然确实没问题,但是我是因为看到 CursorShape 中有 CURSOR_MOVE 才想着用 get_current_cursor_shape 的……简直无敌
https://github.com/khairul169/3rdperson-godot/issues 这位写了一个第三人称的控制器,但是这个项目已经运行不了了
我点开它控制角色的脚本来看,第一时间没看懂,有点乱,也没注释,干脆就不看了
我再搜到的一个是 https://github.com/KevinStirling/ThirdPersonCameraGodot,他这个是写得真的简洁,功能也很正确
extends KinematicBody
export var gravity : int = -12
export var speed : int = 6
export var jump_speed : int = 6
export var air_speed : int = 4
export(float, 0.01, 1) var mouse_sens = 0.05
export(float, -90, 90) var min_camera_angle = -90
export(float, -90, 90) var max_camera_angle = 90
onready var camera : Spatial = $CameraOrbit
var velocity : Vector3 = Vector3()
var jump : bool = false
func _ready():
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func get_input() -> void:
# Handle input and set velocity accordingly
var vy = velocity.y
velocity = Vector3()
var accel = speed if is_on_floor() else air_speed
if Input.is_action_pressed("move_up"):
velocity += -transform.basis.z * accel
if Input.is_action_pressed("move_down"):
velocity += transform.basis.z * accel
if Input.is_action_pressed("move_right"):
velocity += transform.basis.x * accel
if Input.is_action_pressed("move_left"):
velocity += -transform.basis.x * accel
velocity = velocity.normalized() * speed
velocity.y = vy
jump = false
if Input.is_action_just_pressed("jump"):
jump = true
if Input.is_action_just_pressed("ui_cancel"):
if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
else:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func _physics_process(delta) -> void :
# Apply velocity calculation to the physics process
velocity.y += gravity * delta
get_input()
velocity = move_and_slide(velocity, Vector3.UP)
if jump and is_on_floor():
velocity.y = jump_speed
func _unhandled_input(event) -> void:
# Translate mouse movement to camera and character model movement
if event is InputEventMouseMotion:
rotate_y(-lerp(0, mouse_sens, event.relative.x/10))
camera.rotate_x(-lerp(0, mouse_sens, event.relative.y/10))
camera.rotation.x = clamp(camera.rotation.x, deg2rad(min_camera_angle), deg2rad(max_camera_angle))
原来它用的是 _unhandled_input 的 event.relative
我试着在 _unhandled_input 中打印 event.relative,发现他居然是我理想中的鼠标速度
emmmm……给我整不会了,我看文档的时候,它说
The mouse position relative to the previous position (position at the last frame).
我就没想到他这个”相对最后一帧的位置“可以用来表示速度
真的是学到了
然后,鼠标不动的时候 relative 也会一直返回到 0
那我前面说的:
可见,没有 input 就没有 inputevent
鼠标静止的时候,就是没有 input 的时候,所以鼠标静止不会给出一个 inputevent 放到 _unhandled_input 中,所以我的使用 _unhandled_input 获得鼠标移动速度的逻辑有问题,只能获取到最后一次记录的鼠标移动的速度。
就是错的了……
但是这样的话就有点矛盾了。最大的可能是,其实没有 input 的时候,也会有 inputevent 传入 _unhandled_input,只是鼠标静止的时候,speed 为 null,所以打印不出来,给我造成了没有 event 的错觉
这位 KevinStirling 老哥有他自己的一套计算逻辑,我现在暂时先按照我的计算逻辑来的话,就是
extends KinematicBody
# ---对象属性---
# 移动速度
export var moveSpeed = 10
# 下落加速度
export var fallAcceleration = 75
# 线速度
var linearVelocity = Vector3.ZERO
# 鼠标灵敏度
var mouseSensitivity = 0.1
# ---组件引用---
# 弹簧臂
onready var springarm = $SpringArm
# ---控制缓存---
# 物体坐标系二维平面上鼠标运动方向 向上向左为负 向下向右为正
var mouseMoveLocalDir = Vector2(0,0)
# 物体坐标系二维平面上鼠标运动速度大小 视为以玩家为球心,弹簧臂为半径的球上的弧长
var mouseMoveArcLength = 0
# ---事件---
func _unhandled_input(event) -> void:
# 如果获得”鼠标正在运动“事件
if event is InputEventMouseMotion:
# 如果得到鼠标相对于最后一帧的位移不是 0 而是 Vector2,说明鼠标相对于最后一帧移动了
if typeof(event.relative) == TYPE_VECTOR2:
# 获得鼠标在一帧内的移动量
mouseMoveLocalDir = event.relative
mouseMoveArcLength = mouseMoveLocalDir.length()
# ---自定义函数---
# 玩家运动
func playerMove(deltaTime):
# 控制缓存 移动方向
var direction = Vector3.ZERO
# 获取前后左右的移动方向增量
# 注意 xyz 坐标系的方向
if Input.is_action_pressed("move_right"):
direction.x -= 1
if Input.is_action_pressed("move_left"):
direction.x += 1
if Input.is_action_pressed("move_up"):
direction.z += 1
if Input.is_action_pressed("move_down"):
direction.z -= 1
# 移动方向单位化
# Mesh 向移动方向旋转
if direction != Vector3.ZERO:
direction = direction.normalized()
# translation 为父节点的世界坐标,则 translation + direction 为父节点移动方向处长度为 1 的点
$MeshInstance.look_at(translation + direction, Vector3.UP)
# 水平线速度
linearVelocity.x = direction.x * moveSpeed
linearVelocity.z = direction.z * moveSpeed
# Vertical velocity
linearVelocity.y -= fallAcceleration * deltaTime
# Moving the character
linearVelocity = move_and_slide(linearVelocity, Vector3.UP)
# 弹簧臂旋转
func springarmRotate(deltaTime):
# 世界坐标系三维空间中鼠标运动方向
var mouseMoveWorldDir = to_global(Vector3(-mouseMoveLocalDir.x,-mouseMoveLocalDir.y,0))
# 世界坐标系三维空间中玩家前方向
var playerWorldForwardDir = get_global_transform().basis.z
# 世界坐标系三维空间中弹簧臂旋转轴 为三维空间中鼠标运动方向和玩家前方向的叉乘 需要单位化
var springarmRotAxis = (mouseMoveWorldDir.cross(playerWorldForwardDir)).normalized()
# 弹簧臂的旋转角 等于弧长除于半径
var mouseMoveAngle = mouseMoveArcLength/clamp(springarm.get_hit_length(),1,springarm.spring_length)
# 弹簧臂旋转 要考虑物理时间间隔和鼠标灵敏度
springarm.global_rotate(springarmRotAxis,mouseMoveAngle*deltaTime*mouseSensitivity)
# ---虚函数实现---
# 固定帧率执行
func _physics_process(deltaTime):
playerMove(deltaTime)
springarmRotate(deltaTime)
结果还是出现了那个
可见,没有 input 就没有 inputevent
鼠标静止的时候,就是没有 input 的时候,所以鼠标静止不会给出一个 inputevent 放到 _unhandled_input 中,所以我的使用 _unhandled_input 获得鼠标移动速度的逻辑有问题,只能获取到最后一次记录的鼠标移动的速度。
的问题
好奇怪啊,那就是我对自己的否定又是错的了——我原来想的没错?
那别人是怎么做到流畅的旋转的?好吧,别人仅仅是把移动逻辑写到了 _unhandled_input 里面而已
好吧,原来别人就是通过 _unhandled_input 的”鼠标静止的时候,就是没有 input 的时候“的特性,实现鼠标静止的时候,不调用 _unhandled_input,其中的旋转函数不调用,就不旋转,即”鼠标静止就不旋转“的效果的
草……为什么我会纠结这么久
为了统一移动逻辑到 _physics_process 中,我再改成:
extends KinematicBody
# ---对象属性---
# 移动速度
export var moveSpeed : float = 10
# 跳跃速度
export var jumpVelocity : float = 30
# 下落加速度
export var fallAcceleration : float = 100
# 线速度
var linearVelocity : Vector3 = Vector3.ZERO
# 鼠标灵敏度
export var mouseSensitivity : float = 1
# ---组件引用---
# 弹簧臂
onready var springarm = $SpringArm
# ---控制缓存---
# 是否应该旋转弹簧臂
var shouldSpringArmMove : bool = false
# 物体坐标系二维平面上鼠标运动方向 向上向左为负 向下向右为正
var mouseMoveLocalDir = Vector2(0,0)
# 物体坐标系二维平面上鼠标运动速度大小 视为以玩家为球心,弹簧臂为半径的球上的弧长
var mouseMoveArcLength = 0
# ---事件---
func _ready():
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func get_input() -> void:
# 如果按退出键
if Input.is_action_just_pressed("ui_cancel"):
# 在鼠标隐藏和固定之间切换
if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
else:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func _unhandled_input(event) -> void:
# 如果获得”鼠标正在运动“事件
if event is InputEventMouseMotion:
# 如果得到鼠标相对于最后一帧的位移不是 0 而是 Vector2,说明鼠标相对于最后一帧移动了
if typeof(event.relative) == TYPE_VECTOR2:
# 应该旋转弹簧臂
shouldSpringArmMove = true
# 获得鼠标在一帧内的移动量
mouseMoveLocalDir = event.relative
mouseMoveArcLength = mouseMoveLocalDir.length()
# ---自定义函数---
# 玩家运动
func playerMove(deltaTime):
# ---水平方向---
# 控制缓存 移动方向
var direction = Vector3.ZERO
# 获取前后左右的水平移动方向增量
# 注意 xyz 坐标系的方向
if Input.is_action_pressed("move_right"):
direction.x -= 1
if Input.is_action_pressed("move_left"):
direction.x += 1
if Input.is_action_pressed("move_up"):
direction.z += 1
if Input.is_action_pressed("move_down"):
direction.z -= 1
# 水平移动方向单位化
# Mesh 向水平移动方向旋转
if direction != Vector3.ZERO:
direction = direction.normalized()
# translation 为父节点的世界坐标,则 translation + direction 为父节点移动方向处长度为 1 的点
$MeshInstance.look_at(translation + direction, Vector3.UP)
# 水平线速度
linearVelocity = direction * moveSpeed
# ---竖直方向---
# 获取竖直移动方向
if Input.is_action_pressed("jump"):
linearVelocity += Vector3.UP * jumpVelocity
# 施加重力的影响
linearVelocity -= Vector3.UP * fallAcceleration * deltaTime
# 角色移动
linearVelocity = move_and_slide(linearVelocity, Vector3.UP)
# 弹簧臂旋转
func springarmRotate(deltaTime):
# 如果需要旋转弹簧臂
if shouldSpringArmMove:
# 已经开始旋转弹簧臂
shouldSpringArmMove = false
# 世界坐标系三维空间中鼠标运动方向
var mouseMoveWorldDir = to_global(Vector3(-mouseMoveLocalDir.x,mouseMoveLocalDir.y,0))
# 世界坐标系三维空间中玩家前方向
var playerWorldForwardDir = get_global_transform().basis.z
# 世界坐标系三维空间中弹簧臂旋转轴 为三维空间中鼠标运动方向和玩家前方向的叉乘 需要单位化
var springarmRotAxis = (mouseMoveWorldDir.cross(playerWorldForwardDir)).normalized()
# 弹簧臂的旋转角 等于弧长除于半径
var mouseMoveAngle = mouseMoveArcLength/clamp(springarm.get_hit_length(),1,springarm.spring_length)
# 弹簧臂旋转 要考虑物理时间间隔和鼠标灵敏度
springarm.global_rotate(springarmRotAxis,mouseMoveAngle*deltaTime*mouseSensitivity)
# ---虚函数实现---
# 固定帧率执行
func _physics_process(deltaTime):
playerMove(deltaTime)
springarmRotate(deltaTime)
这下旋转是大概没有问题了,但是又出现了一个闪烁的现象
主要原因是,我对弹簧臂的旋转包括了绕 x 轴的旋转。弹簧臂很容易被旋转到水平面之下,撞到地面上,然后收缩,然后直接进入 player 的内部,这样就看不到 player 了;接着再转动一下弹簧臂,假设转动后弹簧臂又回到水平面之上,又回到原来的长度,就又可以看到 player 了,这样一来一回就导致了频闪。
正确的做法应该是,对摄像机绕 x 轴旋转,对弹簧臂绕 y 轴旋转
这么说的话,还是要像他那样子单独 rotate 了
直接用 event.relative.x/10 和 event.relative.y/10 作为 lerp 的参数用于旋转有点不太好,毕竟鼠标移动得快一点就可以让 event.relative 的 x 和 y 大于 10 了,但是他这个是无所谓呃,因为它是用 mouseSensitivity 把每一帧旋转的速度卡死的。这样,如果鼠标移动地很慢,摄像机转动也慢;如果鼠标转动很快,在一定范围内,鼠标移动越快,旋转速度越快,但是当鼠标移动速度大于某一值时,旋转速度不再增加。这就可以防止有人用力过猛导致的混乱,也给出了一个又慢又快的体感。
修改之后:
extends KinematicBody
# ---对象属性---
# 移动速度
export var moveSpeed : float = 10
# 跳跃速度
export var jumpVelocity : float = 30
# 下落加速度
export var fallAcceleration : float = 100
# 线速度
var linearVelocity : Vector3 = Vector3.ZERO
# 鼠标灵敏度
export var mouseSensitivity : float = 0.05
# 鼠标最大移动速度
export var mouseMoveMaxSpeed : float = 10
# ---组件引用---
# 弹簧臂
onready var springarm = $SpringArm
# 摄像机
onready var camera = $SpringArm/Camera
# ---控制缓存---
# 是否应该旋转弹簧臂
var shouldCameraMove : bool = false
# 物体坐标系二维平面上鼠标运动方向 向上向左为负 向下向右为正
var mouseMoveSpeed = Vector2(0,0)
# ---事件---
func _ready():
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func get_input() -> void:
# 如果按退出键
if Input.is_action_just_pressed("ui_cancel"):
# 在鼠标隐藏和固定之间切换
if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
else:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func _unhandled_input(event) -> void:
# 如果获得”鼠标正在运动“事件
if event is InputEventMouseMotion:
# 如果得到鼠标相对于最后一帧的位移不是 0 而是 Vector2,说明鼠标相对于最后一帧移动了
if typeof(event.relative) == TYPE_VECTOR2:
# 应该旋转摄像机
shouldCameraMove = true
# 获得鼠标在一帧内的移动量
mouseMoveSpeed = event.relative
# ---自定义函数---
# 玩家运动
func playerMove(deltaTime):
# ---水平方向---
# 控制缓存 移动方向
var direction = Vector3.ZERO
# 获取前后左右的水平移动方向增量
# 注意 xyz 坐标系的方向
if Input.is_action_pressed("move_right"):
direction.x -= 1
if Input.is_action_pressed("move_left"):
direction.x += 1
if Input.is_action_pressed("move_up"):
direction.z += 1
if Input.is_action_pressed("move_down"):
direction.z -= 1
# 水平移动方向单位化
# Mesh 向水平移动方向旋转
if direction != Vector3.ZERO:
direction = direction.normalized()
# translation 为父节点的世界坐标,则 translation + direction 为父节点移动方向处长度为 1 的点
$MeshInstance.look_at(translation + direction, Vector3.UP)
# 水平线速度
linearVelocity = direction * moveSpeed
# ---竖直方向---
# 获取竖直移动方向
if Input.is_action_pressed("jump"):
linearVelocity += Vector3.UP * jumpVelocity
# 施加重力的影响
linearVelocity -= Vector3.UP * fallAcceleration * deltaTime
# 角色移动
linearVelocity = move_and_slide(linearVelocity, Vector3.UP)
# 摄像机旋转
func cameraRotate(deltaTime):
# 如果需要旋转摄像机
if shouldCameraMove:
# 已经开始旋转摄像机
shouldCameraMove = false
# 旋转摄像机
camera.rotate_x(-lerp(0, mouseSensitivity, mouseMoveSpeed.y/mouseMoveMaxSpeed))
# 旋转弹簧臂
springarm.rotate_y(-lerp(0, mouseSensitivity, mouseMoveSpeed.x/mouseMoveMaxSpeed))
# ---虚函数实现---
# 固定帧率执行
func _physics_process(deltaTime):
playerMove(deltaTime)
cameraRotate(deltaTime)
运行起来还是又闪烁……这就说明我认为是弹簧臂的原因是不正确的
这仍然是摄像机旋转的问题,旋转角度应该还是存在着一些不连续,导致渲染出了问题
我看到一篇教程 https://godottutorials.pro/third-person-controller-tutorial/,他是把摄像机旋转放到了 _process 中
于是我把我的代码改成
# ---虚函数实现---
# 固定帧率执行
func _physics_process(deltaTime):
playerMove(deltaTime)
# 实际帧率执行
func _process(deltaTime):
cameraRotate(deltaTime)
还是会有闪烁的问题……
继续根据教程,我在摄像机旋转的函数中补上对鼠标速度变量的清零
# 摄像机旋转
func cameraRotate(deltaTime):
# 如果需要旋转摄像机
if shouldCameraMove:
# 已经开始旋转摄像机
shouldCameraMove = false
# 旋转摄像机
camera.rotate_x(-lerp(0, mouseSensitivity, mouseMoveSpeed.y/mouseMoveMaxSpeed))
# 旋转弹簧臂
springarm.rotate_y(-lerp(0, mouseSensitivity, mouseMoveSpeed.x/mouseMoveMaxSpeed))
# 重置摄像机速度
mouseMoveSpeed = Vector2.ZERO
还是会有闪烁的问题……
此外这个教程就没有什么东西了
这样的话,其实我和这些流畅的控制器的唯一区别就是,我希望让弹簧臂绕 y 轴旋转,但是他们都是直接将整个角色绕 y 轴旋转。有可能是这个区别导致了我出现闪烁的现象。或者说,弹簧臂绕 y 轴旋转导致了闪烁
测试1 只有摄像机绕 x 轴旋转:
# 摄像机旋转
func cameraRotate(deltaTime):
# 如果需要旋转摄像机
if shouldCameraMove:
# 已经开始旋转摄像机
shouldCameraMove = false
# 旋转摄像机
camera.rotate_x(-lerp(0, mouseSensitivity, mouseMoveSpeed.y/mouseMoveMaxSpeed))
# 旋转弹簧臂
# springarm.rotate_y(-lerp(0, mouseSensitivity, mouseMoveSpeed.x/mouseMoveMaxSpeed))
运行结果
摄像机绕 x 轴旋转正常
测试2 只有弹簧臂绕 y 轴旋转
# 摄像机旋转
func cameraRotate(deltaTime):
# 如果需要旋转摄像机
if shouldCameraMove:
# 已经开始旋转摄像机
shouldCameraMove = false
# 旋转摄像机
# camera.rotate_x(-lerp(0, mouseSensitivity, mouseMoveSpeed.y/mouseMoveMaxSpeed))
# 旋转弹簧臂
springarm.rotate_y(-lerp(0, mouseSensitivity, mouseMoveSpeed.x/mouseMoveMaxSpeed))
运行结果
确实是弹簧臂绕 y 轴旋转导致了闪烁
但是我并不想让整个角色绕 y 轴旋转……这就陷入了僵局
后面我随便乱调的时候,我觉得视野有点不好,就把弹簧臂调高了
然后就神奇地没有闪烁了!
我不知道为什么……
问题暂时解决,于是继续往下写
extends KinematicBody
# ---对象属性---
# 移动速度
export var moveSpeed : float = 10
# 跳跃速度
export var jumpVelocity : float = 30
# 下落加速度
export var fallAcceleration : float = 100
# 线速度
var linearVelocity : Vector3 = Vector3.ZERO
# 鼠标灵敏度
export var mouseSensitivity : float = 0.05
# 鼠标最大移动速度
export var mouseMoveMaxSpeed : float = 10
# 最小俯仰角
export var cameraMinPitch : float = -45
# 最大俯仰角
export var cameraMaxPitch : float = 90
# 角色转身速度
export var playerRotSpeed : float = 0.5
# ---组件引用---
# Mesh
onready var meshes = $Meshes
# 弹簧臂
onready var springarm = $SpringArm
# 摄像机
onready var camera = $SpringArm/Camera
# ---控制缓存---
# 是否应该旋转弹簧臂
var shouldCameraMove : bool = false
# 物体坐标系二维平面上鼠标运动方向 向上向左为负 向下向右为正
var mouseMoveSpeed = Vector2(0,0)
# ---事件---
func _ready():
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func get_input() -> void:
# 如果按退出键
if Input.is_action_pressed("ui_cancel"):
# 在鼠标隐藏和固定之间切换
if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
print("Hi")
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
else:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func _unhandled_input(event) -> void:
# 如果获得”鼠标正在运动“事件
if event is InputEventMouseMotion:
# 如果得到鼠标相对于最后一帧的位移不是 0 而是 Vector2,说明鼠标相对于最后一帧移动了
if typeof(event.relative) == TYPE_VECTOR2:
# 应该旋转摄像机
shouldCameraMove = true
# 获得鼠标在一帧内的移动量
mouseMoveSpeed = event.relative
# ---自定义函数---
# 玩家运动
func playerMove(deltaTime):
# ---水平方向---
# 控制缓存 移动方向
var direction = Vector3.ZERO
# 获取摄像机地前后左右方向
# 注意 xyz 坐标系的方向
if Input.is_action_pressed("move_right"):
direction += camera.get_global_transform().basis.x
if Input.is_action_pressed("move_left"):
direction -= camera.get_global_transform().basis.x
if Input.is_action_pressed("move_up"):
direction -= camera.get_global_transform().basis.z
if Input.is_action_pressed("move_down"):
direction += camera.get_global_transform().basis.z
# 水平移动方向单位化
if direction != Vector3.ZERO:
direction = direction.normalized()
# 水平线速度
linearVelocity = direction * moveSpeed
# ---竖直方向---
# 获取竖直移动方向
if Input.is_action_pressed("jump"):
linearVelocity += Vector3.UP * jumpVelocity
# 施加重力的影响
linearVelocity -= Vector3.UP * fallAcceleration * deltaTime
# 角色移动
linearVelocity = move_and_slide(linearVelocity, Vector3.UP)
# 摄像机旋转
func cameraRotate(deltaTime):
# 如果需要旋转摄像机
if shouldCameraMove:
# 已经开始旋转摄像机
shouldCameraMove = false
# 旋转摄像机
camera.rotate_x(-lerp(0, mouseSensitivity, mouseMoveSpeed.y/mouseMoveMaxSpeed))
# 钳制
camera.rotation_degrees.x = clamp(camera.rotation_degrees.x,cameraMinPitch,cameraMaxPitch)
# 旋转弹簧臂
springarm.rotate_y(-lerp(0, mouseSensitivity, mouseMoveSpeed.x/mouseMoveMaxSpeed))
# 玩家旋转
func meshesRotate(deltaTime):
meshes.rotation_degrees.y = lerp(meshes.rotation_degrees.y, springarm.rotation_degrees.y, playerRotSpeed)
# ---虚函数实现---
# 固定帧率执行
func _physics_process(deltaTime):
playerMove(deltaTime)
cameraRotate(deltaTime)
meshesRotate(deltaTime)
我想让 Mesh 的旋转角跟随弹簧臂的旋转角,但是在某个角度上,Mesh 的旋转角有突变
测试:
# 玩家旋转
func meshesRotate(deltaTime):
print("--------")
print(meshes.rotation_degrees.y)
print(springarm.rotation_degrees.y)
meshes.rotation_degrees.y = lerp(meshes.rotation_degrees.y, springarm.rotation_degrees.y, playerRotSpeed)
print(meshes.rotation_degrees.y)
运行结果:
弹簧臂的 y 方向的旋转角存在一个从 -180 到 180 的突变
这样的话,就不能直接用弹簧臂的 y 方向的旋转角了
只能算旋转增量了
更换旋转方法:
extends KinematicBody
# ---对象属性---
# 移动速度
export var moveSpeed : float = 10
# 跳跃速度
export var jumpVelocity : float = 30
# 下落加速度
export var fallAcceleration : float = 100
# 线速度
var linearVelocity : Vector3 = Vector3.ZERO
# 鼠标灵敏度
export var mouseSensitivity : float = 0.05
# 鼠标最大移动速度
export var mouseMoveMaxSpeed : float = 10
# 最小俯仰角
export var cameraMinPitch : float = -45
# 最大俯仰角
export var cameraMaxPitch : float = 90
# 角色转身速度
export var playerRotSpeed : float = 0.2
# ---组件引用---
# Mesh
onready var meshes = $Meshes
# 弹簧臂
onready var springarm = $SpringArm
# 摄像机
onready var camera = $SpringArm/Camera
# ---控制缓存---
# 是否应该旋转弹簧臂
var shouldCameraMove : bool = false
# 物体坐标系二维平面上鼠标运动方向 向上向左为负 向下向右为正
var mouseMoveSpeed = Vector2(0,0)
# ---事件---
func _ready():
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func get_input() -> void:
# 如果按退出键
if Input.is_action_pressed("ui_cancel"):
# 在鼠标隐藏和固定之间切换
if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
print("Hi")
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
else:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func _unhandled_input(event) -> void:
# 如果获得”鼠标正在运动“事件
if event is InputEventMouseMotion:
# 如果得到鼠标相对于最后一帧的位移不是 0 而是 Vector2,说明鼠标相对于最后一帧移动了
if typeof(event.relative) == TYPE_VECTOR2:
# 应该旋转摄像机
shouldCameraMove = true
# 获得鼠标在一帧内的移动量
mouseMoveSpeed = event.relative
# ---自定义函数---
# 玩家运动
func playerMove(deltaTime):
# ---水平方向---
# 控制缓存 移动方向
var direction = Vector3.ZERO
# 获取摄像机地前后左右方向
# 注意 xyz 坐标系的方向
if Input.is_action_pressed("move_right"):
direction += camera.get_global_transform().basis.x
if Input.is_action_pressed("move_left"):
direction -= camera.get_global_transform().basis.x
if Input.is_action_pressed("move_up"):
direction -= camera.get_global_transform().basis.z
if Input.is_action_pressed("move_down"):
direction += camera.get_global_transform().basis.z
# 水平移动方向单位化
if direction != Vector3.ZERO:
direction = direction.normalized()
# 水平线速度
linearVelocity = direction * moveSpeed
# ---竖直方向---
# 获取竖直移动方向
if Input.is_action_pressed("jump"):
linearVelocity += Vector3.UP * jumpVelocity
# 施加重力的影响
linearVelocity -= Vector3.UP * fallAcceleration * deltaTime
# 角色移动
linearVelocity = move_and_slide(linearVelocity, Vector3.UP)
# 摄像机旋转
func cameraRotate(deltaTime):
# 如果需要旋转摄像机
if shouldCameraMove:
# 已经开始旋转摄像机
shouldCameraMove = false
# 旋转摄像机
camera.rotate_x(-lerp(0, mouseSensitivity, mouseMoveSpeed.y/mouseMoveMaxSpeed))
# 钳制
camera.rotation_degrees.x = clamp(camera.rotation_degrees.x,cameraMinPitch,cameraMaxPitch)
# 旋转弹簧臂
springarm.rotate_y(-lerp(0, mouseSensitivity, mouseMoveSpeed.x/mouseMoveMaxSpeed))
# 玩家模型旋转
func meshesRotate(deltaTime):
# meshes 前方向
var meshesForwardVector = meshes.get_global_transform().basis.z
# 弹簧臂 前方向 由于我弹簧臂摆放的设置,这个获得的前方向和期望的前方向是相反的
var springarmForwardVector = -springarm.get_global_transform().basis.z
# meshes 前方向 和 弹簧臂 前方向 之间的夹角
var angle = meshesForwardVector.angle_to(springarmForwardVector)
# 从 meshes 前方向 到 弹簧臂 前方向 的向量
var deltaVector = springarmForwardVector - meshesForwardVector
# rotate_x 增加的方向是逆时针方向
# 如果从 meshes 前方向 到 弹簧臂 前方向 是顺时针方向,就把 angle 设为负
if deltaVector.dot(meshes.get_global_transform().basis.x) < 0:
angle = -angle
# 应用角色转身速度
angle *= playerRotSpeed
# meshes 旋转
meshes.rotate_y(angle)
# ---虚函数实现---
# 固定帧率执行
func _physics_process(deltaTime):
playerMove(deltaTime)
cameraRotate(deltaTime)
meshesRotate(deltaTime)
运行结果:
旋转确实是已经做好了
但是还是有一点小问题,就是感觉跳跃太快了,因为他这个跳跃是直接给一个速度
但是 KinematicBody 还真就没有物理模拟的函数,所以还是要自己实现一个
实现结果:
终于起码是可以看得过眼了
http://godot.pro/doc/tutorials/inputs/inputevent.html
官方文档 inputevent
https://*.com/questions/48438273/godot-3d-get-forward-vector
获得物体的前方向
https://*.com/questions/62844337/godot-how-would-i-get-inputeventmousemotion-in-the-process-function
获取 inputeventmousemotion 的方法
https://github.com/khairul169/3rdperson-godot/issues
一个老的第三人称工程
https://github.com/KevinStirling/ThirdPersonCameraGodot
一个最近的第三人称工程
https://godottutorials.pro/third-person-controller-tutorial/
制作第三人称控制器的图文教程
https://www.youtube.com/watch?v=SIGnJLtgk7w&ab_channel=Zenva
制作第三人称控制器的视频教程