🎮

Godot4でキャラクター操作

2023/09/30に公開

ノードを配置していく

シーンに床を配置する

まずは適当に床のノードを用意
下の階層になるようにノードを追加していきます
大きさは適当で、BOXの上面が高さ0になるようにしてます
当たり判定のあるオブジェクトはこのようになるのが一般的みたい

ノードの追加方法

親ノードを選択して「+」ボタン
親ノードを選択して「Ctrl+A」
親ノードを選択して右クリックから「子ノードを追加」

警告が出るのを修正する

作業しているとツリーのところに警告のアイコンが出ることがあります
ノードを使う上で問題になる部分を警告くしてくれるので修正します
ここでは子ノードにCollisionShape3Dを作成し、shapeを設定すればOKです

3Dモデルをプロジェクトに追加

キャラクター用のモデルを用意します
自分はMixamoから拾ってきました
アニメーション付きでglb形式でダウンロードしています
ダウンロードしたglbをファイルシステムにドラッグしてプロジェクトに追加します

3Dモデルからシーンを作成して配置

3Dモデルにはキャラクターの操作やその他いろいろ組み込むのでシーンを作成してから配置します
ファイルシステムのglbファイルを右クリックして新しい継承シーンで
glbからシーンを作成します。名前は「player.tscn」にしています

作成したシーンを保存し、ファイルシステムからドラッグしてゲームに配置します

カメラの配置

キャラクタの子にカメラを置いてもいいですが、今回はキャラクターと別にカメラ用のノードを作っていきます

CameraControllerとCameraPitchはNode3Dで作成し名前を変更しています
SpringArm3Dはバネのように伸び縮みしてカメラが壁にめり込むのを防いでくれます
キャラクタの移動はカメラの向きに依存しますが、上下の回転は無関係なのでCameraPitchにして分けています
こうするとキャラクタの移動にCameraControllerの姿勢を加工せずにそのまま使うことができます

キャラクタ操作の実装

スクリプトでキャラクタの操作を実装していきます
スクリプトを作成する前にいくつか設定しておきます

ノードの型を変更する

キャラクタのシーンのルートノードの型を変更します
ルートノードを選択、右クリックから「型の変更」を選びます
型はCharacterBody3Dを選びます

スクリプトをアタッチする

playerノードを選択して右クリックから「スクリプトのアタッチ」を選びます

型の変更がうまくできていると継承元とテンプレートのところがCharacterBody3Dになっているはずです
このテンプレートを使うとキャラ操作が簡単に作成できます

警告が出るのを修正する

ここでも警告が出るのでCharacterBody3Dの子ノードにCollisionShapeを作成しshapeをカプセルにしておきます

CollisionShapeがキャラクタを覆うように大きさ、位置を調整しておきます

インプットマップの設定

スクリプトの修正の前にいくつか設定をしておきます
メニューの「プロジェクト>プロジェクトの設定」から操作キーの設定ができます
「新しいアクションの追加」のところにleftなど入れて「追加」を押します
右の「+」ボタンから設定するキーの追加ができます
今回は sprint/left/right/up/down/jump があればよいです

グループの設定

グループの機能はシーン間でノードの参照をする時などに使用するようです
メインのシーンで「player」を選択した状態で、インスペクタの右にある「ノード>グループ」から空欄に「player」を入力し「追加」を押します

「player」グループのグループ内ノードに「Node3D/player」があればOKです
同様に「camera_controller」に「Node3D/CameraController」を設定しておきます

キャラクターのスクリプトを修正する

準備ができたのでスクリプトを修正していきます
全文は畳んでおきます

player.gd
extends CharacterBody3D

# @export を付けて変数を宣言するとインスペクタで値を設定できるようになる
@export var JUMP_VELOCITY = 4.5
@export var WALK_SPEED = 5.0
@export var SPRINT_SPEED = 10.0
@export var LERP_SPEED = 10
var current_speed
var direction = Vector3.ZERO

# Get the gravity from the project settings to be synced with RigidBody nodes.
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
var camera_controller: Node3D

func _ready():
	# マウスが画面外に出ないようになる。終了するときはAlt+F4などを使う
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
	# グループ機能を使ってCameraControllerを取得する。結果が配列なので0番目をとる
	camera_controller = get_tree().get_nodes_in_group("camera_controller")[0]

func _physics_process(delta):

	# インプットマップで設定したsprintボタンが押されている場合、速度を上げる
	if Input.is_action_pressed("sprint"):
		current_speed = SPRINT_SPEED
	else:
		current_speed = WALK_SPEED

	# Add the gravity.
	if not is_on_floor():
		velocity.y -= gravity * delta

	# Handle Jump.
	if Input.is_action_just_pressed("jump") and is_on_floor():
		velocity.y = JUMP_VELOCITY

	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
	# インプットマップ
	var input_dir = Input.get_vector("left", "right", "up", "down")
	direction = lerp( direction, (camera_controller.transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized(), delta * LERP_SPEED)
	if direction:
		look_at(global_position + direction)
		velocity.x = direction.x * current_speed
		velocity.z = direction.z * current_speed
	else:
		velocity.x = move_toward(velocity.x, 0, current_speed)
		velocity.z = move_toward(velocity.z, 0, current_speed)

	move_and_slide()

速度関連の値を設定します
@exportを指定するとインスペクタから値の設定が行えるようになります

@export var JUMP_VELOCITY = 4.5
@export var WALK_SPEED = 5.0
@export var SPRINT_SPEED = 10.0
@export var LERP_SPEED = 10
var current_speed
var direction = Vector3.ZERO

_readyはゲームの最初に1度だけ呼ばれる処理です
グループ機能で取得した結果は配列なので注意

func _ready():
	# マウスが画面外に出ないようになる。終了するときはAlt+F4などを使う
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
	# グループ機能を使ってCameraControllerを取得する。結果が配列なので0番目をとる
	camera_controller = get_tree().get_nodes_in_group("camera_controller")[0]

_physics_processや_processは毎フレーム処理される関数です
deltaはフレーム間の時間です
移動処理などを自前で行う場合はdeltaをかければ大体うまくいきます
ダッシュ(Shiftキー)で速度を変えています

func _physics_process(delta):

	# インプットマップで設定したsprintボタンが押されている場合、速度を上げる
	if Input.is_action_pressed("sprint"):
		current_speed = SPRINT_SPEED
	else:
		current_speed = WALK_SPEED

lerp関数は二つの値を 割合を指定して補完する関数です
方向キーを押した向きに瞬時に向かずに少しずつ向くようにしています
そのために前回の方向を保存するdirection変数を作って
今回入力された方向に少しずつ向くようにしています
camera_controllerを使用しているのはカメラの向きを基準に移動するためです
pitchを分けたのはここで使うためです

	var input_dir = Input.get_vector("left", "right", "up", "down")
	direction = lerp( direction, (camera_controller.transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized(), delta * LERP_SPEED)

look_at関数は指定した方向を向くように回転を行う関数です
global_positionは自身(playerノード)のワールド内での位置です

	if direction:
		look_at(global_position + direction)
		velocity.x = direction.x * current_speed
		velocity.z = direction.z * current_speed
	else:
		velocity.x = move_toward(velocity.x, 0, current_speed)
		velocity.z = move_toward(velocity.z, 0, current_speed)

move_and_slide関数はCharacterBody3Dのテンプレートで実装されています
ここで色々面倒な処理をやってくれています

カメラのスクリプトを修正する

少ないのでまとめて解説します

@onreadyの行はシーンのツリーから「CameraPitch」をCtrlを押しながらスクリプトにドラッグすると追加できます
同じシーン内のノードで変更がないノードはこれでアクセスすればよいです

_process関数内でplayerノードの位置を毎回コピーして追従するようにしています

_input関数はキーやマウスの入力があったときに呼ばれる関数です
ここではマウスの移動のイベントであるかを判定し回転を行っています

clamp関数は値の最大値/最小値を指定して、それを超えないように値を修正してくれる関数です
これをしないとカメラの上下の回転でどこまでも回転して裏返ったりします

extends Node3D

@export var mouse_sensitivity: float = 0.5
@onready var camera_pitch = $CameraPitch
var player

# Called when the node enters the scene tree for the first time.
func _ready():
	player = get_tree().get_nodes_in_group("player")[0]

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	global_position = player.global_position

func _input(event):
	if event is InputEventMouseMotion:
		rotate_y(deg_to_rad(-event.relative.x * mouse_sensitivity))
		camera_pitch.rotate_x(deg_to_rad(-event.relative.y * mouse_sensitivity))
		camera_pitch.rotation.x = clamp(camera_pitch.rotation.x,deg_to_rad(-80),deg_to_rad(20))

長くなってきたのでアニメーション等はまた次の機会に

Discussion