[Godot][GDScript] ThirdPersonController 第三人称 3C 的实现

  1. 完整代码

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)

布局:

[Godot][GDScript] ThirdPersonController 第三人称 3C 的实现

  1. 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)

这下旋转是大概没有问题了,但是又出现了一个闪烁的现象

[Godot][GDScript] ThirdPersonController 第三人称 3C 的实现

主要原因是,我对弹簧臂的旋转包括了绕 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)

运行起来还是又闪烁……这就说明我认为是弹簧臂的原因是不正确的

[Godot][GDScript] ThirdPersonController 第三人称 3C 的实现

这仍然是摄像机旋转的问题,旋转角度应该还是存在着一些不连续,导致渲染出了问题

我看到一篇教程 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))

运行结果

[Godot][GDScript] ThirdPersonController 第三人称 3C 的实现

摄像机绕 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))
		

运行结果

[Godot][GDScript] ThirdPersonController 第三人称 3C 的实现

确实是弹簧臂绕 y 轴旋转导致了闪烁

但是我并不想让整个角色绕 y 轴旋转……这就陷入了僵局
后面我随便乱调的时候,我觉得视野有点不好,就把弹簧臂调高了

[Godot][GDScript] ThirdPersonController 第三人称 3C 的实现

然后就神奇地没有闪烁了!
我不知道为什么……

问题暂时解决,于是继续往下写

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 的旋转角有突变

[Godot][GDScript] ThirdPersonController 第三人称 3C 的实现

测试:

# 玩家旋转
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)

运行结果:

[Godot][GDScript] ThirdPersonController 第三人称 3C 的实现

旋转确实是已经做好了
但是还是有一点小问题,就是感觉跳跃太快了,因为他这个跳跃是直接给一个速度
但是 KinematicBody 还真就没有物理模拟的函数,所以还是要自己实现一个

实现结果:

[Godot][GDScript] ThirdPersonController 第三人称 3C 的实现

终于起码是可以看得过眼了

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
制作第三人称控制器的视频教程

上一篇:iptables控制


下一篇:python学习打卡(1)