]> Untitled Git - william-skin.git/blob - player/player.gd
6c78b210855e3a1c3cb040049381cbf11f73b16c
[william-skin.git] / player / player.gd
1 class_name Player
2 extends CharacterBody3D
3
4
5 # signals
6 signal camera_lockon
7
8
9 # player settings
10 @export_group("Movement")
11 @export var walk_speed := 5.0
12 @export var jog_speed := 10.0
13 @export var air_speed := 3.0
14 @export var acceleration := 30.0
15 @export var jump_speed := 6.0
16 @export var rotation_speed := 10.0
17 @export var fall_speed := 1.2
18 @export var idle_timeout := 5.0
19 @export var hard_landing_limit := 10.0
20
21
22 @export_group("Camera")
23 @export_range(1.0, 10.0) var camera_distance := 2.0
24 @export_range(0.0, 100.0) var camera_lockon_radius := 40.0
25 @export_enum("locked", "unlocked") var camera_mode: String
26 @export_range(0.0, 50.0) var lockon_sensitivity := 0.2
27 @export_range(0.0, 1.0) var mouse_sensitivity := 0.25
28 @export_range(0.0, 10.0) var joystick_sensitivity_x := 4.0
29 @export_range(0.0, 10.0) var joystick_sensitivity_y := 2.0
30
31
32 @export_group("Expressions")
33 @export_range(1.0, 100.0) var head_rotation_speed := 5.0
34
35
36 @onready var _camera_pivot: Node3D = $cameraPivot
37 @onready var _camera_spring: SpringArm3D = $cameraPivot/SpringArm3D
38 @onready var _camera: Camera3D = $cameraPivot/SpringArm3D/Camera3D
39 @onready var _skin: WilliamSkin = %WilliamSkin
40 @onready var _debug: CanvasLayer = %DebugOverlay
41
42
43 # class variables
44 var _player_speed := walk_speed
45 var _camera_input_direction := Vector2.ZERO
46 var _last_movement_direction := rotation
47 var _idle_time := 0.0
48 var _camera_lockon_node: Node3D = null
49 var _lockon_direction: Vector3 = Vector3.ZERO
50 var _lockon_indicator_scene = load("res://ux/lockon_indicator.tscn")
51 var _lockon_instance: Node3D = _lockon_indicator_scene.instantiate()
52
53 var _head_track_arr: Array[Node3D] = []
54
55 enum {LOCKON_LEFT, LOCKON_CENTER, LOCKON_RIGHT}
56 var _lockon_shift := LOCKON_CENTER
57
58 enum {CAMERA_MOUSE_INPUT, CAMERA_JOYSTICK_INPUT}
59 var _camera_input_method := CAMERA_MOUSE_INPUT
60
61 enum _states {FREE, POSING, TALKING}
62 var _player_state := _states.FREE
63
64
65 # inherited functions
66 func _ready() -> void:
67         _camera_spring.spring_length = camera_distance
68         
69         # debug
70         _debug.draw.add_vector(self, "velocity", 1, 3, Color(0,1,0,1))
71         _debug.draw.add_vector(self, "_lockon_direction", 1, 2, Color(1,0,0,1))
72         _debug.draw.add_vector(_camera_pivot, "rotation", 1, 3, Color(1,0,1,1))
73         _debug.draw.add_vector(self, "_last_movement_direction", 1, 3, Color(1,0,0,1))
74         
75         _debug.stats.add_property(self, "velocity", "")
76         _debug.stats.add_property(self, "_idle_time", "round")
77         _debug.stats.add_property(self, "_lockon_shift", "round")
78         _debug.stats.add_property(self, "_head_track_to", "")
79         _debug.stats.add_property(self, "_head_track_arr", "")
80
81
82 func _input(event: InputEvent) -> void:
83         if event.is_action_pressed("camera-lockon"):
84                 camera_lockon.emit()
85         elif event.is_action_pressed("player-run"):
86                 _player_speed = jog_speed
87         elif event.is_action_released("player-run"):
88                 _player_speed = walk_speed
89         elif event.is_action_pressed("player-pose"):
90                 _player_state = _states.POSING
91         elif event.is_action_released("player-pose"):
92                 _player_state = _states.FREE
93         elif event.is_action_pressed("player-action"):
94                 # TODO: contextual action
95                 if _player_state == _states.FREE:
96                         _player_state = _states.TALKING
97                         velocity = Vector3.ZERO
98                 elif _player_state == _states.TALKING:
99                         _player_state = _states.FREE
100
101
102 func _unhandled_input(event: InputEvent) -> void:
103         # If user clicks on the window, capture the mouse and direct the camera with it
104         if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
105                 return
106                 
107                 #_camera_input_direction *= mouse_sensitivity
108         if event is InputEventMouseMotion:
109                 _camera_input_method = CAMERA_MOUSE_INPUT
110                 _camera_input_direction = event.screen_relative * mouse_sensitivity
111         elif event is InputEventJoypadMotion:
112                 _camera_input_method = CAMERA_JOYSTICK_INPUT
113                 _camera_input_direction = Input.get_vector("camera-left", "camera-right", "camera-up", "camera-down")
114                 if _camera_input_direction == Vector2.ZERO:
115                         _lockon_shift = LOCKON_CENTER
116                 _camera_input_direction *= Vector2(joystick_sensitivity_x, -joystick_sensitivity_y)
117
118
119 # class functions 
120 func _physics_process(delta: float) -> void:
121         _process_player(delta)
122         _process_camera(delta)
123
124
125 func is_camera_locked() -> bool:
126         return camera_mode == "locked"
127
128
129 func set_camera_lockon(arr: Array[Node]) -> void:
130         if _camera_lockon_node:
131                 _camera_lockon_node.remove_child(_lockon_instance)
132
133         if not is_camera_locked() or _lockon_shift != LOCKON_CENTER:
134                 # pick closest lockon point to center of camera view
135                 # must be within lockon radius
136                 var shortest_target_angle = _camera.fov / 360 * 2 * PI 
137                 for n: Node3D in arr:
138                         if _camera_lockon_node == n:
139                                 continue
140                                 
141                         var target_dir := _camera_pivot.global_position - n.global_position
142                         if target_dir.length() < camera_lockon_radius:
143                                 var target_angle = shortest_target_angle
144                                 if _lockon_shift == LOCKON_CENTER:
145                                         var camera_dir = _camera.global_position - _camera_pivot.global_position
146                                         target_angle = target_dir.angle_to(camera_dir)
147                                 else:
148                                         # check to make sure if lockon switch is a node to the left if player pressed left,
149                                         # else lockon switch to something to the right
150                                         target_angle = target_dir.signed_angle_to(_lockon_direction, Vector3.UP)
151                                         if _lockon_shift == LOCKON_RIGHT and target_angle <= 0:
152                                                 continue
153                                         if _lockon_shift == LOCKON_LEFT and target_angle >= 0:
154                                                 continue
155                                                 
156                                         target_angle = abs(target_angle)
157                                 if target_angle < shortest_target_angle:
158                                         shortest_target_angle = target_angle
159                                         _camera_lockon_node = n
160                 
161                 if _camera_lockon_node:
162                         _camera_lockon_node.add_child(_lockon_instance)
163                         camera_mode = "locked"
164         else:
165                 camera_mode = "unlocked"
166                 _camera_lockon_node = null
167                 _lockon_direction = Vector3.ZERO
168                 
169                 # TODO: move all skin stuff to its own reset function
170                 _skin.move_forward()
171                 _skin.set_hips_direction(0)
172                 
173                 # always reset shift to center!
174                 _lockon_shift = LOCKON_CENTER
175
176
177 func _process_camera(delta: float) -> void:
178         if not is_camera_locked():
179                 # vertical camera rotation
180                 _camera_pivot.rotation.x += _camera_input_direction.y * delta
181                 _camera_pivot.rotation.x = clamp(_camera_pivot.rotation.x, -PI / 6, PI / 3)
182
183                 # horizontal camera rotation
184                 _camera_pivot.rotation.y -= _camera_input_direction.x * delta
185
186                 # reset mouse movement vector if mouse input
187                 if _camera_input_method == CAMERA_MOUSE_INPUT:
188                         _camera_input_direction = Vector2.ZERO
189         else:
190                 # camera should lerp to a set height
191                 _camera_pivot.rotation.x = lerp(_camera_pivot.rotation.x, 0.0, delta)
192                 _camera_pivot.rotation.z = lerp(_camera_pivot.rotation.z, 0.0, delta)
193                 
194                 # keep camera pivot pointed towards locked object
195                 if _camera_lockon_node:
196                         # calculate target vector between node position and player
197                         var lock_vector := _camera_pivot.global_position - _camera_lockon_node.global_position
198                         _lockon_direction = lock_vector
199                         lock_vector.y = 0
200                         var target_angle = Vector3.BACK.signed_angle_to(lock_vector, Vector3.UP)
201                         _camera_pivot.global_rotation.y = lerp_angle(_camera_pivot.global_rotation.y, target_angle, rotation_speed * delta)
202                                 
203                         # if player indicates right or left, emit lockon signal again to collect array and split it
204                         if _camera_input_direction.x < -lockon_sensitivity and _lockon_shift != LOCKON_LEFT:
205                                 _lockon_shift = LOCKON_LEFT
206                                 camera_lockon.emit()
207                         elif _camera_input_direction.x > lockon_sensitivity and _lockon_shift != LOCKON_RIGHT:
208                                 _lockon_shift = LOCKON_RIGHT
209                                 camera_lockon.emit()
210
211
212 # Get the XZ input direction based on player's input relative to the camera
213 func _get_player_move_direction() -> Vector3:
214         var input_dir := Input.get_vector("player-left", "player-right", "player-forward", "player-backward")
215         var forward := _camera.global_basis.z
216         var right := _camera.global_basis.x
217         var move_direction := (forward * input_dir.y + right * input_dir.x).normalized()
218         move_direction.y = 0
219         return move_direction
220
221
222 func _process_player_floor(move_direction: Vector3, delta: float) -> void:
223         # if player is landing, then just return
224         if _skin.current_state() == "landing":
225                 return
226         elif _skin.current_state() == "fall":
227                 if velocity.length() > hard_landing_limit:
228                         velocity = Vector3.ZERO
229                         _skin.landing()
230                         return
231                 else:
232                         _skin.move_forward()
233         
234         # if we're not stuck, then it's okay to set the velocity
235         velocity = velocity.move_toward(move_direction * _player_speed, acceleration * delta)   
236         
237         # if player jumps, then we're done
238         if Input.is_action_just_pressed("player-jump"):
239                 velocity.y = jump_speed
240                 _idle_time = 0.0
241                 _skin.jump()
242                 return
243
244         # also, if we're moving, we're not idle
245         # last movement direction required for skin orientation
246         if move_direction.length() < 0.2:
247                 if velocity == Vector3.ZERO:
248                         _idle_time += delta
249                 if _idle_time > idle_timeout:
250                         _skin.idle()
251                         return
252         else:
253                 _last_movement_direction = move_direction
254                 _idle_time = 0.0
255
256         # now handle skin rotation and animation
257         var movement_speed := Vector3(velocity.x, 0, velocity.z).length()
258         _skin.movement_speed(movement_speed)
259         
260         # if camera is unlocked, rotate whole skin to face movement direction
261         # else, rotate to face camera pivot global Y direction
262         var skin_target_angle := Vector3.BACK.signed_angle_to(_last_movement_direction, Vector3.UP)
263         
264         # lean into momentum just a little bit
265         _skin.rotation.z = lerp_angle(
266                 _skin.rotation.z,
267                 clamp(_last_movement_direction.signed_angle_to(velocity, Vector3.UP) * movement_speed * 0.08, -PI/4, PI/ 4),
268                 rotation_speed * delta * 0.25
269                 )
270
271         if not is_camera_locked():
272                 _skin.move_forward()
273                         
274                 _skin.global_rotation.y = lerp_angle(
275                         _skin.global_rotation.y, 
276                         skin_target_angle, 
277                         rotation_speed * delta
278                         )
279         else: # camera is locked
280                 var movement_angle := _skin.global_rotation.y - skin_target_angle
281                 if abs(movement_angle) < (PI / 2):
282                         _skin.move_forward()
283                 else:
284                         _skin.move_backward()
285                         
286                 _skin.global_rotation.y = lerp_angle(
287                         _skin.global_rotation.y, 
288                         _camera_pivot.global_rotation.y - PI, 
289                         rotation_speed * delta)
290                         
291                 # hips rotate towards the direction of movement
292                 # else hips rotate toward skin global Y direction
293                 # if moving backwards, then target_angle is inverted 
294                 if movement_speed > 0.1:
295                         var target_hips_dir := lerp_angle(
296                                         _skin.global_rotation.y + _skin.get_hips_direction(), 
297                                         skin_target_angle if abs(movement_angle) < (PI / 2) else (skin_target_angle - PI), 
298                                         rotation_speed * delta
299                                         ) - _skin.global_rotation.y
300                         _skin.set_hips_direction(target_hips_dir)
301                 else:
302                         _skin.set_hips_direction(lerp_angle(_skin.get_hips_direction(), 0, rotation_speed * delta))
303
304
305 func _process_player_falling(move_direction: Vector3, delta: float) -> void:
306         velocity += get_gravity() * fall_speed * delta
307         velocity += move_direction * air_speed * delta
308         _skin.fall()
309
310
311 func _process_player(delta: float) -> void:
312         var move_direction := _get_player_move_direction()
313
314         if is_on_floor():
315                 if _player_state == _states.POSING:
316                         _skin.pose()
317                 elif _player_state == _states.TALKING:
318                         _skin.talk()
319                 else:
320                         _process_player_floor(move_direction, delta)
321         else:
322                 _process_player_falling(move_direction, delta)
323         
324         move_and_slide()
325
326
327 func _on_head_turn_area_entered(area: Area3D) -> void:
328         for node in area.get_parent().get_children().filter(func(c): return c.is_in_group("player_head_turn")):
329                 var i = _head_track_arr.find(node)
330                 if i < 0:
331                         _head_track_arr.append(node)
332
333
334 func _on_head_turn_area_exited(area: Area3D) -> void:
335         for node in area.get_parent().get_children().filter(func(c): return c.is_in_group("player_head_turn")):
336                 var i = _head_track_arr.find(node)
337                 if i >= 0:
338                         _head_track_arr.remove_at(i)