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

本文详细记录了使用Godot游戏引擎和GDScript开发第三人称控制器的过程,探讨了在处理鼠标输入和旋转逻辑时遇到的问题,如鼠标静止时的旋转、弹簧臂旋转与闪烁现象,并分享了解决方案。通过学习官方文档、StackOverflow讨论及开源项目,最终成功实现了平滑的第三人称控制器。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  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.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值