class_name PlayerController extends Node3D ## ## ## enum SelectAction { NONE, PRIMARY, SECONDARY, } ## ## Supported selection input devices. ## enum SelectMode { NONE, MOUSE, } ## ## The device being used for selection has changed. ## signal select_mode_changed(mode: SelectMode) ## ## Selection of a point on screenspace has started happening. ## signal selection_started() ## ## Selection of a point on screenspace has stopped happening. ## signal selection_stopped() const _BACKWARD := "player_controller_backward" const _FORWARD := "player_controller_forward" const _LEFT := "player_controller_left" const _RIGHT := "player_controller_right" const _ROTATE_CCW := "player_controller_rotate_ccw" const _ROTATE_CW := "player_controller_rotate_cw" const _DRAG_SPEED_BASE_MODIFIER := 0.15 const _MOVE_SMOOTHING := 0.5 const _MOVE_SPEED_BASE_MODIFIER := 50.0 const _ROTATE_SPEED_BASE := 5.0 const _TRANSFORM_DELTA := 10.0 @export var _camera: Camera3D = null var _control_override_count := 0 var _cursor_point := Vector2.ZERO var _is_drag_panning := false var _select_action := SelectAction.NONE var _select_mode := SelectMode.NONE @export var _selection_area: Control = null @onready var _target_position := position @onready var _target_orientation := global_rotation.y func _input(event: InputEvent) -> void: if event is InputEventMouseButton: match event.button_index: MOUSE_BUTTON_MIDDLE: _is_drag_panning = event.is_pressed() and not(is_frozen()) Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if\ _is_drag_panning else Input.MOUSE_MODE_VISIBLE MOUSE_BUTTON_LEFT: if event.is_pressed(): _select_action = SelectAction.PRIMARY selection_started.emit() else: _select_action = SelectAction.NONE selection_stopped.emit() MOUSE_BUTTON_RIGHT: if event.is_pressed(): _select_action = SelectAction.SECONDARY selection_started.emit() else: _select_action = SelectAction.NONE selection_stopped.emit() return if (event is InputEventMouseMotion) and _is_drag_panning: var global_basis := global_transform.basis var camera_settings := GameSettings.camera_settings var dampened_speed := camera_settings.movement_speed_modifier * _DRAG_SPEED_BASE_MODIFIER _target_position += dampened_speed.y * (-global_basis.z) *\ event.relative.y * (-1.0 if camera_settings.is_y_inverted else 1.0) _target_position += dampened_speed.x * (-global_basis.x) *\ event.relative.x * (-1.0 if camera_settings.is_x_inverted else 1.0) return if event is InputEventScreenDrag: return if event is InputEventScreenTouch: return func _process(delta: float) -> void: if not(is_frozen()): var global_basis := global_transform.basis var camera_settings := GameSettings.camera_settings var delta_speed :=\ camera_settings.movement_speed_modifier * _MOVE_SPEED_BASE_MODIFIER * delta _target_position += delta_speed.y * (-global_basis.z) *\ (Input.get_action_strength(_FORWARD) - Input.get_action_strength(_BACKWARD)) *\ (-1.0 if camera_settings.is_y_inverted else 1.0) _target_position += delta_speed.x * (-global_basis.x) *\ (Input.get_action_strength(_LEFT) - Input.get_action_strength(_RIGHT)) *\ (-1.0 if camera_settings.is_y_inverted else 1.0) _target_orientation += (Input.get_action_strength(_ROTATE_CCW) -\ Input.get_action_strength(_ROTATE_CW)) * _ROTATE_SPEED_BASE *\ camera_settings.rotation_speed_modifier * delta global_transform = global_transform.interpolate_with( Transform3D(Basis(Vector3.UP, _target_orientation), _target_position), delta * _TRANSFORM_DELTA * _MOVE_SMOOTHING) match _select_mode: SelectMode.NONE: _cursor_point = get_viewport().size / 2.0 SelectMode.MOUSE: _cursor_point = get_viewport().get_mouse_position() func _ready() -> void: if _selection_area != null: _selection_area.mouse_entered.connect(func () -> void: if _select_mode != SelectMode.MOUSE: _select_mode = SelectMode.MOUSE select_mode_changed.emit(SelectMode.MOUSE)) _selection_area.mouse_exited.connect(func () -> void: if _select_mode != SelectMode.NONE: _select_mode = SelectMode.NONE select_mode_changed.emit(SelectMode.NONE)) ## ## Returns the position of the selection cursor with respect to the select current mode being used. ## ## When a mouse input device is being used, this will be its location in screen coordinates. For ## touchscreen interactions, this will be the location of an active gesture. Finally, for all other ## states - including gamepad - this will be the middle of screenspace at all times. ## func get_cursor_point() -> Vector2: return _cursor_point func is_frozen() -> bool: assert(_control_override_count > -1, "control override count cannot be less than 0") return _control_override_count != 0 ## ## ## func in_select_area() -> bool: return _select_mode != SelectMode.NONE ## ## Returns the [code]SelectAction[/code] currently being performed, or ## [code]SelectAction.NONE[/code] if no action is currently being performed. ## func get_select_action() -> SelectAction: return _select_action ## ## ## func override_controls(unlock_signal: Signal) -> void: assert(_control_override_count > -1, "control override count cannot be less than 0") _control_override_count += 1 unlock_signal.connect(func () -> void: assert(_control_override_count > 0, "control override count cannot be less than 1") _control_override_count -= 1, Object.CONNECT_ONE_SHOT) ## ## Attempts to convert the screen coordinates in [code]screen_point[/code] into 2D worldspace ## coordinate relative to an infinite ground plane at y offset [code]0.0[/code]. ## ## Successful point intersection will return the plane coordinates in a [class Vector2], otherwise ## [code]null[/code] is returned. ## func screen_to_plane(screen_point: Vector2) -> Variant: if _camera != null: var plane_target = Plane(Vector3.UP, 0.0).intersects_ray( _camera.global_position, _camera.project_ray_normal(screen_point)) if plane_target is Vector3: return Vector2(plane_target.x, plane_target.z) return null