|
| 1 | +extends CharacterBody3D |
| 2 | + |
| 3 | +# Define our FPS and TPS player views using Euler angles. |
| 4 | +var _yaw : float = 0 |
| 5 | +var _pitch = 0 |
| 6 | + |
| 7 | +# XZ direction the player is looking at. |
| 8 | +var _direction = Vector3(-sin(_yaw), 0, -cos(_yaw)) |
| 9 | + |
| 10 | +# TPS Camera. |
| 11 | +var _tps_camera_proximity = 3 |
| 12 | +var _tps_camera_look_from = Vector3() |
| 13 | + |
| 14 | +# Fixed camera, first person shooter, third person shooter. |
| 15 | +enum CameraType {CAM_FIXED, CAM_FPS, CAM_TPS} |
| 16 | + |
| 17 | +# Current camera type. |
| 18 | +# (Note we toggle this in _ready() so it actually starts with FPS camera.) |
| 19 | +var _cam_type = CameraType.CAM_FIXED |
| 20 | + |
| 21 | +var _bullet_scene = load("res://bullet.tscn") |
| 22 | + |
| 23 | +func _ready(): |
| 24 | + # Capture the mouse, stop the cursor showing. |
| 25 | + Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) |
| 26 | + |
| 27 | + # We define the TPS camera in global space by setting it |
| 28 | + # as top_level so it ignores the parent transform. |
| 29 | + $Rig/Camera_TPS.set_as_top_level(true) |
| 30 | + |
| 31 | + # Perform the logic to create FPS view to start with. |
| 32 | + toggle_camera_type() |
| 33 | + |
| 34 | +# Input can come in freely at any point during the frame or tick. |
| 35 | +# We use this to transform the player rig immediately (so no lag), |
| 36 | +# but this means we should turn off physics interpolation for the rig |
| 37 | +# (we use the physics_interpolation_mode in the inspector, but the mode |
| 38 | +# can also be changed via script). |
| 39 | +func _input(event: InputEvent) -> void: |
| 40 | + if event is InputEventMouseMotion: |
| 41 | + _yaw -= event.relative.x * 0.005 # How much we react to left right mouse. |
| 42 | + _pitch += event.relative.y * 0.01 # How much we react to up down mouse. |
| 43 | + _pitch = clamp(_pitch, -PI, PI) # Don't look higher or lower than 90 degrees. |
| 44 | + |
| 45 | + # Apply only the yaw to the rig (we don't want the body mesh to change pitch). |
| 46 | + # Pitch is handled separately. |
| 47 | + $Rig.rotation = Vector3(0, _yaw, 0) |
| 48 | + |
| 49 | +func _update_camera(_delta: float): |
| 50 | + # Keep the player direction up to date based on the yaw. |
| 51 | + _direction.x = -sin(_yaw) |
| 52 | + _direction.z = -cos(_yaw) |
| 53 | + |
| 54 | + # Rotate the head (and FPS camera and firing origin) with the |
| 55 | + # pitch from the mouse. |
| 56 | + $Rig/Head.rotation = Vector3(_pitch * -0.5, 0, 0) |
| 57 | + |
| 58 | + if _cam_type == CameraType.CAM_TPS: |
| 59 | + # We will focus the TPS camera on the head of the player. |
| 60 | + var target = $Rig/Head.get_global_transform_interpolated().origin |
| 61 | + |
| 62 | + # Calculate a position to look at the player from. |
| 63 | + var pos = target |
| 64 | + |
| 65 | + # The camera should be behind the player, so reverse the polarity |
| 66 | + # of direction. |
| 67 | + pos.x -= _direction.x * _tps_camera_proximity |
| 68 | + pos.z -= _direction.z * _tps_camera_proximity |
| 69 | + |
| 70 | + # Move the TPS camera up and down depending on the pitch. |
| 71 | + # There's no special formula here, just something that looks okay. |
| 72 | + pos.y += 2.0 + (_pitch * (0.2 * _tps_camera_proximity)) |
| 73 | + |
| 74 | + # Offset from the old _tps_camera_look_from to the new position |
| 75 | + # we want the TPS camera to move to. |
| 76 | + var offset = pos - _tps_camera_look_from |
| 77 | + var l = offset.length() |
| 78 | + |
| 79 | + # We cap how far we allow the TPS camera to move on each update, |
| 80 | + # so we get a smooth movement, rather than snapping. |
| 81 | + var tps_cam_speed = _delta * 8.0 |
| 82 | + |
| 83 | + # If we are trying to move further than the maximum allowed, |
| 84 | + # we resize the offset to tps_cam_speed. |
| 85 | + if (l > tps_cam_speed): |
| 86 | + offset *= tps_cam_speed / l |
| 87 | + |
| 88 | + # Move the TPS camera. |
| 89 | + _tps_camera_look_from += offset |
| 90 | + |
| 91 | + # The look_at_from_position does all the magic for us. |
| 92 | + $Rig/Camera_TPS.look_at_from_position(_tps_camera_look_from, target, Vector3(0, 1, 0)) |
| 93 | + |
| 94 | + # For a real TPS camera some other things to try: |
| 95 | + # * Ray cast from the player towards the camera to prevent it looking through walls. |
| 96 | + # * Try smoothing the camera by yaw / pitch from the player rather than offset. |
| 97 | + |
| 98 | +func toggle_camera_type(): |
| 99 | + match _cam_type: |
| 100 | + CameraType.CAM_FIXED: |
| 101 | + _cam_type = CameraType.CAM_FPS |
| 102 | + $Rig/Head/Camera_FPS.make_current() |
| 103 | + CameraType.CAM_FPS: |
| 104 | + _cam_type = CameraType.CAM_TPS |
| 105 | + $Rig/Camera_TPS.make_current() |
| 106 | + CameraType.CAM_TPS: |
| 107 | + _cam_type = CameraType.CAM_FIXED |
| 108 | + get_node("../Camera_Fixed").make_current() |
| 109 | + |
| 110 | + # Hide body in FPS view, show in all other views. |
| 111 | + $Rig/Mesh_Body.visible = _cam_type != CameraType.CAM_FPS |
| 112 | + |
| 113 | +func _process(_delta: float) -> void: |
| 114 | + # Allow selecting different cameras. |
| 115 | + if (Input.is_action_just_pressed("ui_focus_next")): |
| 116 | + toggle_camera_type() |
| 117 | + |
| 118 | + if (Input.is_action_just_pressed("fire")): |
| 119 | + # Create a bullet... |
| 120 | + var bul = _bullet_scene.instantiate() |
| 121 | + |
| 122 | + # Figure out where we want the bullet to spawn. |
| 123 | + # We use a dummy Node offset from the head, but you may want to use e.g. |
| 124 | + # a bone attachment, or dummy node on a weapon. |
| 125 | + var tr : Transform3D = $Rig/Head/Fire_Origin.get_global_transform_interpolated() |
| 126 | + bul.position = tr.origin |
| 127 | + |
| 128 | + # We can calculate the direction the bullet should travel from the basis (rotation) |
| 129 | + # of the dummy Node. |
| 130 | + var bul_dir = tr.basis[2].normalized() |
| 131 | + |
| 132 | + # Let's give our physics bullet some velocity. |
| 133 | + bul.linear_velocity = bul_dir * -9 |
| 134 | + get_node("..").add_child(bul) |
| 135 | + |
| 136 | + # A moving start for a bullet using physics interpolation can be done |
| 137 | + # by resetting, THEN offsetting the position in the direction of travel. |
| 138 | + # This means that on the first tick the bullet will be moving rather than |
| 139 | + # standing still, which can look unnatural. |
| 140 | + bul.reset_physics_interpolation() |
| 141 | + bul.position -= bul_dir * (1.0 - Engine.get_physics_interpolation_fraction()) |
| 142 | + |
| 143 | + |
| 144 | + # If we pressed reset, or too far from the origin... move the player back to origin. |
| 145 | + if (Input.is_action_just_pressed("ui_accept") or position.length() > 10): |
| 146 | + position = Vector3(0, 1, 0) |
| 147 | + velocity = Vector3() |
| 148 | + reset_physics_interpolation() |
| 149 | + _yaw = 0 |
| 150 | + _pitch = 0 |
| 151 | + |
| 152 | + if (Input.is_action_just_pressed("jump")) and is_on_floor(): |
| 153 | + velocity += Vector3(0, 12, 0) |
| 154 | + |
| 155 | + # We update our camera every frame. |
| 156 | + # Our camera is not physics interpolated, as we want fast response from the mouse. |
| 157 | + # However in the case of FPS and TPS, the position is indirectly inherited from |
| 158 | + # the physics interpolated player, so we get nice smooth motion, but quick mouse |
| 159 | + # response. |
| 160 | + _update_camera(_delta) |
| 161 | + |
| 162 | +# When physics interpolation is active on the node, |
| 163 | +# you should move it on the physics tick (physics_process) |
| 164 | +# rather than on the frame (process). |
| 165 | +func _physics_process(_delta: float) -> void: |
| 166 | + |
| 167 | + var move : Vector3 = Vector3() |
| 168 | + |
| 169 | + # Let us calculate a move relative to the player coordinate system. |
| 170 | + if Input.is_action_pressed("ui_up"): |
| 171 | + move.z -= 1 |
| 172 | + if Input.is_action_pressed("ui_down"): |
| 173 | + move.z += 1 |
| 174 | + if Input.is_action_pressed("ui_left"): |
| 175 | + move.x -= 1 |
| 176 | + if Input.is_action_pressed("ui_right"): |
| 177 | + move.x += 1 |
| 178 | + |
| 179 | + # Apply some gravity. |
| 180 | + move.y -= 0.9 |
| 181 | + |
| 182 | + # Apply mouse rotation to the move, so that it is now in global space. |
| 183 | + move = move.rotated(Vector3(0, 1, 0), _yaw) |
| 184 | + |
| 185 | + # Apply the global space move to the physics. |
| 186 | + velocity += move |
| 187 | + |
| 188 | + move_and_slide() |
| 189 | + |
| 190 | + # Apply some friction. |
| 191 | + velocity *= 0.9 |
0 commit comments