ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2022-01-23 13:33:18  阅读:184  来源: 互联网

标签:Godot 鼠标 direction 旋转 --- ThirdPersonController GDScript var Input


  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)

布局:

在这里插入图片描述

  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://stackoverflow.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://stackoverflow.com/questions/48438273/godot-3d-get-forward-vector
获得物体的前方向
 
https://stackoverflow.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
制作第三人称控制器的视频教程

标签:Godot,鼠标,direction,旋转,---,ThirdPersonController,GDScript,var,Input
来源: https://blog.csdn.net/PriceCheap/article/details/122642661

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有