Skip to content

Commit 30992f3

Browse files
committed
Implement rain, water sounds and player death, add README
- Add large plane surrounding the level with invisible walls to prevent the player from falling out.
1 parent 96f1444 commit 30992f3

22 files changed

+252
-48
lines changed

3d/first_person_shooter/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# First-Person Shooter
2+
3+
This is a demo implementing a retro-styled first-person shooter with a 2.5D
4+
aesthetic (level is 3D, but enemies are 2D sprites).
5+
6+
Controls:
7+
8+
- Mouse, arrow keys or <kbd>Gamepad Right Stick</kbd>: Look around
9+
- <kbd>W</kbd>, <kbd>Gamepad Left Stick</kbd>: Move forward
10+
- <kbd>S</kbd>, <kbd>Gamepad Left Stick</kbd>: Move backward
11+
- <kbd>A</kbd>, <kbd>Gamepad Left Stick</kbd>: Move left
12+
- <kbd>D</kbd>, <kbd>Gamepad Left Stick</kbd>: Move right
13+
- <kbd>Space</kbd>, <kbd>Right mouse button</kbd>, <kbd>Gamepad A/Cross</kbd>: Jump
14+
- <kbd>Left mouse button</kbd>, <kbd>Gamepad Right Trigger</kbd>: Attack (or respawn if dead)
15+
- <kbd>F</kbd>, <kbd>Thumb mouse buttons</kbd>, <kbd>Gamepad B/Circle</kbd>: Toggle flashlight
16+
- <kbd>Escape</kbd>, <kbd>Gamepad D-Pad Up</kbd>: Quit
17+
18+
Language: GDScript
19+
20+
Renderer: Forward Plus
21+
22+
## How does it work?
23+
24+
The base vehicle uses a
25+
[`VehicleBody`](https://docs.godotengine.org/en/latest/classes/class_vehiclebody.html)
26+
node. The trailer truck is tied together using a
27+
[`ConeJointTwist`](https://docs.godotengine.org/en/latest/classes/class_conetwistjoint.html)
28+
node, and the tow truck is tried together using a chain made of
29+
[`RigidBody`](https://docs.godotengine.org/en/latest/classes/class_rigidbody.html)
30+
nodes which are pinned together using
31+
[`PinJoint`](https://docs.godotengine.org/en/latest/classes/class_pinjoint.html) nodes.
32+
33+
## Screenshot
34+
35+
![Screenshot](screenshots/first_person_shooter.webp)
36+
37+
## License
38+
39+
- `player/shotgun/spritesheet.png`, `player/shotgun/*.wav` and
40+
`enemy/spritesheet.png` are Copyright © 2001-2022
41+
[Contributors to the Freedoom project](https://freedoom.github.io/)
42+
and are licensed under
43+
[3-clause BSD](https://github.com/freedoom/freedoom/blob/master/COPYING.adoc).
44+
- `player/water_splash_in.ogg` and `player/water_splash_out.ogg` are Copyright ©
45+
2009-2019 [Red Eclipse Team](https://www.redeclipse.net/) and are licensed under
46+
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
47+
- `player/rain.ogg` is
48+
[Copyright © Paul Hertz ("ignotus")](https://freesound.org/people/ignotus/sounds/14779/)
49+
and is licensed under
50+
[CC BY 3.0](https://creativecommons.org/licenses/by/3.0/).
51+
- `fonts/vga-rom-font.png` is edited from
52+
<https://doomwiki.org/wiki/File:Vga-rom-font.png>, which is considered to be
53+
ineligible for copyright and therefore in the public domain.

3d/first_person_shooter/box.tscn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@ surface_material_override/0 = SubResource("StandardMaterial3D_ka0y5")
5353
shape = SubResource("BoxShape3D_abq30")
5454

5555
[node name="GPUParticlesCollisionBox3D" type="GPUParticlesCollisionBox3D" parent="."]
56-
extents = Vector3(0.63, 0.63, 0.63)
56+
size = Vector3(1.26, 1.26, 1.26)

3d/first_person_shooter/enemy/enemy.gd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func _on_line_of_sight_timer_timeout() -> void:
7676
# Allow performing queries immediately after enabling the RayCast.
7777
# Otherwise, we would have to wait one physics frame.
7878
$LineOfSight.force_raycast_update()
79-
can_see_player = not $LineOfSight.is_colliding()
79+
can_see_player = player.health >= 1 and not $LineOfSight.is_colliding()
8080

8181
# Disable RayCast once it's not needed anymore (until the next timer timeout) to improve performance.
8282
$LineOfSight.enabled = false

3d/first_person_shooter/enemy/enemy.tscn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ tracks/1/keys = {
9696
}],
9797
"times": PackedFloat32Array(0.1)
9898
}
99+
tracks/1/use_blend = true
99100
tracks/2/type = "value"
100101
tracks/2/imported = false
101102
tracks/2/enabled = true
@@ -205,6 +206,7 @@ mesh = SubResource("QuadMesh_he27t")
205206
surface_material_override/0 = SubResource("ShaderMaterial_4tdqe")
206207

207208
[node name="Decal" parent="." instance=ExtResource("4_nscyu")]
209+
size = Vector3(1.2, 2.4, 1.2)
208210
cull_mask = 1048569
209211

210212
[node name="WeaponSound" type="AudioStreamPlayer3D" parent="."]
525 Bytes
Binary file not shown.

3d/first_person_shooter/level.GPUParticlesCollisionSDF3D_data.exr.import

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ dest_files=["res://.godot/imported/level.GPUParticlesCollisionSDF3D_data.exr-544
1616
[params]
1717

1818
compress/mode=3
19+
compress/high_quality=false
1920
compress/lossy_quality=0.7
2021
compress/hdr_compression=1
21-
compress/bptc_ldr=0
2222
compress/channel_pack=1
2323
mipmaps/generate=false
2424
mipmaps/limit=-1

3d/first_person_shooter/level.tscn

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[gd_scene load_steps=28 format=3 uid="uid://bcmepm05qldy5"]
1+
[gd_scene load_steps=29 format=3 uid="uid://bcmepm05qldy5"]
22

33
[ext_resource type="Texture2D" uid="uid://bc3n2sen32mqx" path="res://sky.png" id="1_0n6b1"]
44
[ext_resource type="PackedScene" uid="uid://dfyswwfb8j6iv" path="res://player.tscn" id="2_cwuyg"]
@@ -120,6 +120,8 @@ _data = {
120120
"water_surface": SubResource("Animation_as77u")
121121
}
122122

123+
[sub_resource type="WorldBoundaryShape3D" id="WorldBoundaryShape3D_vy5j4"]
124+
123125
[node name="Node3D" type="Node3D"]
124126

125127
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
@@ -169,6 +171,12 @@ use_collision = true
169171
size = Vector3(100, 40, 100)
170172
material = ExtResource("3_b18pi")
171173

174+
[node name="Outside" type="CSGBox3D" parent="Main"]
175+
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -21, 0)
176+
use_collision = true
177+
size = Vector3(10000, 40, 10000)
178+
material = ExtResource("3_b18pi")
179+
172180
[node name="Inner" type="CSGBox3D" parent="Main"]
173181
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 16, 0)
174182
operation = 2
@@ -220,7 +228,7 @@ shape = SubResource("BoxShape3D_04k8c")
220228
[node name="GPUParticlesCollisionBox3D" type="GPUParticlesCollisionBox3D" parent="Main/Water"]
221229
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -3, 0)
222230
cull_mask = 4294967293
223-
extents = Vector3(7.5, 3, 15)
231+
size = Vector3(15, 6, 30)
224232

225233
[node name="AnimationPlayer" type="AnimationPlayer" parent="Main/Water"]
226234
autoplay = "water_surface"
@@ -229,7 +237,7 @@ libraries = {
229237
}
230238

231239
[node name="Building" type="CSGBox3D" parent="."]
232-
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4, 3, 36)
240+
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4, 3, 36.5)
233241
use_collision = true
234242
size = Vector3(16, 25, 16)
235243
material = ExtResource("3_b18pi")
@@ -267,7 +275,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5, -8.25, -10)
267275
distance = 20.5
268276

269277
[node name="Building3" type="CSGBox3D" parent="."]
270-
transform = Transform3D(0.995645, 0, 0.0932221, 0, 1, 0, -0.0932221, 0, 0.995645, -36.2054, 3, 36.0096)
278+
transform = Transform3D(0.995645, 0, 0.0932221, 0, 1, 0, -0.0932221, 0, 0.995645, -36.2054, 3, 36.5096)
271279
use_collision = true
272280
size = Vector3(16, 25, 16)
273281
material = ExtResource("3_b18pi")
@@ -428,7 +436,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 22.25, -9.5, 16)
428436

429437
[node name="GPUParticlesCollisionSDF3D" type="GPUParticlesCollisionSDF3D" parent="."]
430438
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.5, 0)
431-
extents = Vector3(51, 17, 51)
439+
size = Vector3(102, 34, 102)
432440
resolution = 3
433441
bake_mask = 4294967281
434442
texture = ExtResource("5_myd3n")
@@ -473,20 +481,47 @@ transform = Transform3D(0.5, 0, 0.866025, 0, 1, 0, -0.866026, 0, 0.5, -7, -8.875
473481
[node name="Box4" parent="Boxes" instance=ExtResource("5_o3uqf")]
474482
transform = Transform3D(0.965926, 0, -0.258819, 0, 1, 0, 0.258819, 0, 0.965926, -8.5, -8.875, 36.5)
475483

476-
[node name="Box5" parent="Boxes" instance=ExtResource("5_o3uqf")]
477-
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 28, -10, 39.25)
478-
479484
[node name="Box9" parent="Boxes" instance=ExtResource("5_o3uqf")]
480485
transform = Transform3D(0.5, 0, -0.866025, 0, 1, 0, 0.866025, 0, 0.5, 22.25, -8.875, 42.25)
481486

482-
[node name="Box6" parent="Boxes" instance=ExtResource("5_o3uqf")]
483-
transform = Transform3D(0.906603, 0.421985, 0, -0.421985, 0.906603, 0, 0, 0, 1, 26.8351, -10.0595, 29.75)
487+
[node name="Box10" parent="Boxes" instance=ExtResource("5_o3uqf")]
488+
transform = Transform3D(2.08616e-07, 0, -1, 0, 1, 0, 1, 0, 2.08616e-07, 20.75, -8.875, 40.75)
489+
490+
[node name="Box11" parent="Boxes" instance=ExtResource("5_o3uqf")]
491+
transform = Transform3D(-0.707106, 0, -0.707107, 0, 1, 0, 0.707107, 0, -0.707106, 10.4697, 12.125, 14.1686)
484492

485-
[node name="Box8" parent="Boxes" instance=ExtResource("5_o3uqf")]
486-
transform = Transform3D(-0.234646, -0.109218, -0.965926, -0.421985, 0.906603, 0, 0.875711, 0.407607, -0.258819, 34.3351, -10.0595, 38.5)
493+
[node name="Box12" parent="Boxes" instance=ExtResource("5_o3uqf")]
494+
transform = Transform3D(-0.965926, 0, -0.258819, 0, 1, 0, 0.258819, 0, -0.965926, 11.5303, 12.125, 12.3314)
495+
496+
[node name="Box13" parent="Boxes" instance=ExtResource("5_o3uqf")]
497+
transform = Transform3D(-0.707106, 0, -0.707107, 0, 1, 0, 0.707107, 0, -0.707106, 8.46967, 16.125, 38.6686)
498+
499+
[node name="Box14" parent="Boxes" instance=ExtResource("5_o3uqf")]
500+
transform = Transform3D(-0.965926, 0, -0.258819, 0, 1, 0, 0.258819, 0, -0.965926, 1.03033, 16.125, 31.8314)
501+
502+
[node name="Box15" parent="Boxes" instance=ExtResource("5_o3uqf")]
503+
transform = Transform3D(-0.965926, 0, -0.258819, 0, 1, 0, 0.258819, 0, -0.965926, -33.7197, 6.125, 25.3314)
487504

488505
[node name="Box7" parent="Boxes" instance=ExtResource("5_o3uqf")]
489506
transform = Transform3D(0.5, 0, 0.866025, 0, 1, 0, -0.866026, 0, 0.5, -5.7476, -8.875, 34.6871)
490507

508+
[node name="InvisibleWalls" type="StaticBody3D" parent="."]
509+
510+
[node name="CollisionShape3D" type="CollisionShape3D" parent="InvisibleWalls"]
511+
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 2.16371e-06, -49.5)
512+
shape = SubResource("WorldBoundaryShape3D_vy5j4")
513+
514+
[node name="CollisionShape3D2" type="CollisionShape3D" parent="InvisibleWalls"]
515+
transform = Transform3D(-1, -8.74228e-08, 3.82137e-15, 0, -4.37114e-08, -1, 8.74228e-08, -1, 4.37114e-08, 0, -2.16371e-06, 49.5)
516+
shape = SubResource("WorldBoundaryShape3D_vy5j4")
517+
518+
[node name="CollisionShape3D3" type="CollisionShape3D" parent="InvisibleWalls"]
519+
transform = Transform3D(-4.37114e-08, 1, 3.82137e-15, -4.37114e-08, 0, -1, -1, -4.37114e-08, 4.37114e-08, -49.5, -4.58969e-07, 10.5)
520+
shape = SubResource("WorldBoundaryShape3D_vy5j4")
521+
522+
[node name="CollisionShape3D4" type="CollisionShape3D" parent="InvisibleWalls"]
523+
transform = Transform3D(-4.37114e-08, -1, 0, -4.37114e-08, 0, -1, 1, -4.37114e-08, -4.37114e-08, 49.5, -2.16371e-06, 13.75)
524+
shape = SubResource("WorldBoundaryShape3D_vy5j4")
525+
491526
[connection signal="body_entered" from="Main/Water" to="Main/Water" method="_on_body_entered"]
492527
[connection signal="body_exited" from="Main/Water" to="Main/Water" method="_on_body_exited"]

3d/first_person_shooter/platform.tscn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ mesh = SubResource("BoxMesh_tg3xl")
5757
surface_material_override/0 = SubResource("StandardMaterial3D_y10mx")
5858

5959
[node name="GPUParticlesCollisionBox3D" type="GPUParticlesCollisionBox3D" parent="."]
60-
extents = Vector3(3, 0.25, 2)
60+
size = Vector3(6, 0.5, 4)
6161

6262
[node name="MoveTrigger" type="Area3D" parent="."]
6363
collision_layer = 2

3d/first_person_shooter/player.gd

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ var health: int = 100:
6767
$Crosshair.modulate.g = crosshair_color.g
6868
$Crosshair.modulate.b = crosshair_color.b
6969

70+
if health <= 0:
71+
die()
72+
7073
# Time counter for view bobbing (doesn't increment while airborne).
7174
var bob_cycle_counter := 0.0
7275
# Fall impact accumulator for camera landing effect.
@@ -81,17 +84,26 @@ var damage_roll := 0.0
8184
var touching_ground := false
8285

8386
# `true` if the player is currently in water, `false` otherwise.
84-
var in_water := false
87+
var in_water := false:
88+
set(value):
89+
if value != in_water:
90+
# Water state has changed, play sound accordingly.
91+
if value:
92+
$WaterInSound.play()
93+
else:
94+
$WaterOutSound.play()
95+
96+
in_water = value
8597

8698
# The height of the water plane the player is currently in (in global Y coordinates).
8799
# This is used to check whether the camera is underwater to apply effects.
88100
# When not in water, this is set to negative infinity to ensure checks against it are always `false`.
89101
var water_plane_y := -INF
90102

91-
var base_height := ProjectSettings.get_setting("display/window/size/viewport_height")
103+
var base_height: int = ProjectSettings.get_setting("display/window/size/viewport_height")
92104

93105
# Get the gravity from the project settings to be synced with RigidBody nodes.
94-
var gravity := ProjectSettings.get_setting("physics/3d/default_gravity")
106+
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
95107

96108
@onready var initial_camera_position: Vector3 = $Camera3D.position
97109
@onready var initial_weapon_sprite_position: Vector3 = $Camera3D/WeaponSprite.position
@@ -136,7 +148,11 @@ func _physics_process(delta: float) -> void:
136148

137149
# Perform view bobbing based on horizontal movement speed, and also apply the fall bobbing offset.
138150
# We can't use `v_offset` as it would depend on view pitch.
139-
$Camera3D.position.y = initial_camera_position.y + sin(bob_cycle_counter * 10) * 0.01 * Vector3(velocity.x, 0, velocity.z).length() - bob_fall_counter
151+
if health >= 1:
152+
$Camera3D.position.y = initial_camera_position.y + sin(bob_cycle_counter * 10) * 0.01 * Vector3(velocity.x, 0, velocity.z).length() - bob_fall_counter
153+
else:
154+
# Put camera near the floor level while dead.
155+
$Camera3D.position.y = -0.65
140156

141157
# Perform weapon sprite bobbing (horizontal and vertical), and also apply the fall bobbing offset on the weapon sprite.
142158
$Camera3D/WeaponSprite.position.x = initial_weapon_sprite_position.x + cos(bob_cycle_counter * 10) * 0.002 * Vector3(velocity.x, 0, velocity.z).length()
@@ -147,7 +163,12 @@ func _physics_process(delta: float) -> void:
147163

148164
# Roll the camera based on sideways movement speed.
149165
var roll := velocity.dot($Camera3D.transform.basis.x)
150-
$Camera3D.rotation.z = -roll * 0.003 + damage_roll
166+
if health >= 1:
167+
$Camera3D.rotation.z = -roll * 0.003 + damage_roll
168+
else:
169+
# Roll camera permanently and hide the weapon sprite when dead.
170+
$Camera3D.rotation.z = deg_to_rad(-75)
171+
$Camera3D/WeaponSprite.visible = false
151172

152173
# Character controller.
153174

@@ -166,7 +187,8 @@ func _physics_process(delta: float) -> void:
166187
velocity.y -= gravity * delta
167188

168189

169-
if Input.is_action_pressed("jump") and (in_water or touching_ground):
190+
if health >= 1 and Input.is_action_pressed("jump") and (in_water or touching_ground):
191+
# Only allow jumping while alive.
170192
if in_water:
171193
# Allow jumping while in water to swim upwards, but more slowly.
172194
# Here, jumping is performed every frame if underwater, so multiply it by `delta`.
@@ -175,8 +197,12 @@ func _physics_process(delta: float) -> void:
175197
velocity.y = JUMP_VELOCITY
176198

177199
# Get the input direction and handle the movement/deceleration.
178-
# As good practice, you should replace UI actions with custom gameplay actions.
179-
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_backward")
200+
var input_dir := Vector2()
201+
if health >= 1:
202+
input_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_backward")
203+
else:
204+
# Don't allow player movement while dead, but still process gravity and water damping.
205+
input_dir = Vector2()
180206

181207
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
182208
var motion := Vector3()
@@ -202,8 +228,6 @@ func _physics_process(delta: float) -> void:
202228

203229
if not Input.is_action_pressed("jump") and touching_ground:
204230
# Stair climbing.
205-
# FIXME: Prevent camera from snapping downwards after landing from a jump.
206-
# FIXME: Check collision normal to prevent stepping on steep slopes (> 46 degrees).
207231
global_position.y = 1.25 + $ShapeCast3D.get_collision_point(0).y
208232
# Apply downwards velocity to stick to slopes and small steps.
209233
velocity.y = -5.0
@@ -233,8 +257,12 @@ func _process(delta: float) -> void:
233257
$Camera3D.rotation.x = clampf($Camera3D.rotation.x + look.x * LOOK_SENSITIVITY * delta, MIN_VIEW_PITCH, MAX_VIEW_PITCH)
234258
$Camera3D.rotation.y -= look.y * LOOK_SENSITIVITY * delta
235259

236-
# Shooting.
260+
# Shooting (or respawning if currently dead).
237261
if Input.is_action_pressed("attack") and is_zero_approx($ShootTimer.time_left):
262+
if health <= 0:
263+
get_tree().reload_current_scene()
264+
return
265+
238266
for i in SHOTGUN_BULLET_COUNT:
239267
var bullet = preload("res://bullet.tscn").instantiate()
240268
# Bullets are not child of the player to prevent moving along the player.
@@ -256,6 +284,9 @@ func _process(delta: float) -> void:
256284
else:
257285
$Crosshair.modulate.a = 1.0
258286

287+
# Hide crosshair if dead.
288+
$Crosshair.visible = health >= 1
289+
259290
# Fade out damage effect over time.
260291
$DamageEffect.modulate.a = lerpf($DamageEffect.modulate.a, 0.0, 3 * delta)
261292
damage_roll = lerpf(damage_roll, 0.0, 4 * delta)
@@ -278,3 +309,8 @@ func _input(event: InputEvent) -> void:
278309

279310
if event.is_action_pressed(&"quit"):
280311
get_tree().quit()
312+
313+
314+
# Called when the player dies.
315+
func die() -> void:
316+
$HUD/Health.text = "You died! Click to retry."

0 commit comments

Comments
 (0)