1+ /* ******************************************************************************************
2+ *
3+ * raylib [core] example - 3d camera fps
4+ *
5+ * Example complexity rating: [★★★☆] 3/4
6+ *
7+ * Example originally created with raylib 5.5, last time updated with raylib 5.5
8+ *
9+ * Example contributed by Agnis Aldins (@nezvers) and reviewed by Ramon Santamaria (@raysan5)
10+ *
11+ * Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
12+ * BSD-like license that allows static linking with closed source software
13+ *
14+ * Copyright (c) 2025 Agnis Aldins (@nezvers)
15+ *
16+ ********************************************************************************************/
17+
18+ package raylib_examples
19+
20+ import " core:math/linalg"
21+ import rl " vendor:raylib"
22+
23+ // ----------------------------------------------------------------------------------
24+ // Defines and Macros
25+ // ----------------------------------------------------------------------------------
26+ // Movement constants
27+ GRAVITY :: 32.0
28+ MAX_SPEED :: 20.0
29+ CROUCH_SPEED :: 5.0
30+ JUMP_FORCE :: 12.0
31+ MAX_ACCEL :: 150.0
32+ // Grounded drag
33+ FRICTION :: 0.86
34+ // Increasing air drag, increases strafing speed
35+ AIR_DRAG :: 0.98
36+ // Responsiveness for turning movement direction to looked direction
37+ CONTROL :: 15.0
38+ CROUCH_HEIGHT :: 0.0
39+ STAND_HEIGHT :: 1.0
40+ BOTTOM_HEIGHT :: 0.5
41+
42+ NORMALIZE_INPUT :: false
43+
44+ // ----------------------------------------------------------------------------------
45+ // Types and Structures Definition
46+ // ----------------------------------------------------------------------------------
47+ // Body structure
48+ Body :: struct {
49+ position: rl.Vector3,
50+ velocity: rl.Vector3,
51+ dir: rl.Vector3,
52+ isGrounded: bool ,
53+ }
54+
55+ // ----------------------------------------------------------------------------------
56+ // Global Variables Definition
57+ // ----------------------------------------------------------------------------------
58+ sensitivity := rl.Vector2{0.001 , 0.001 }
59+
60+ player: Body
61+ lookRotation: rl.Vector2
62+ headTimer: f32
63+ walkLerp: f32
64+ headLerp: f32 = STAND_HEIGHT
65+ lean: rl.Vector2
66+
67+ // ------------------------------------------------------------------------------------
68+ // Program main entry point
69+ // ------------------------------------------------------------------------------------
70+ main :: proc () {
71+ // Initialization
72+ // --------------------------------------------------------------------------------------
73+ SCREEN_WIDTH :: 800
74+ SCREEN_HEIGHT :: 450
75+
76+ rl.InitWindow (SCREEN_WIDTH, SCREEN_HEIGHT, " raylib [core] example - 3d camera fps" )
77+
78+ // Initialize camera variables
79+ // NOTE: UpdateCameraFPS() takes care of the rest
80+ camera := rl.Camera {
81+ fovy = 60.0 ,
82+ projection = .PERSPECTIVE,
83+ position = { player.position.x, player.position.y + (BOTTOM_HEIGHT + headLerp), player.position.z },
84+ }
85+
86+ update_camera_fps (&camera) // Update camera parameters
87+
88+ rl.DisableCursor () // Limit cursor to relative movement inside the window
89+
90+ rl.SetTargetFPS (60 ) // Set our game to run at 60 frames-per-second
91+ // --------------------------------------------------------------------------------------
92+
93+ // Main game loop
94+ for !rl.WindowShouldClose () { // Detect window close button or ESC key
95+ // Update
96+ // ----------------------------------------------------------------------------------
97+ mouseDelta := rl.GetMouseDelta ()
98+ lookRotation.x -= mouseDelta.x * sensitivity.x
99+ lookRotation.y += mouseDelta.y * sensitivity.y
100+
101+ sideway := i8 (rl.IsKeyDown (.D)) - i8 (rl.IsKeyDown (.A))
102+ forward := i8 (rl.IsKeyDown (.W)) - i8 (rl.IsKeyDown (.S))
103+ crouching := rl.IsKeyDown (.LEFT_CONTROL)
104+ update_body (&player, lookRotation.x, sideway, forward, rl.IsKeyPressed (.SPACE), crouching)
105+
106+ delta := rl.GetFrameTime ()
107+ headLerp = rl.Lerp (headLerp, (crouching ? CROUCH_HEIGHT : STAND_HEIGHT), 20.0 * delta)
108+ camera.position = { player.position.x, player.position.y + (BOTTOM_HEIGHT + headLerp), player.position.z }
109+
110+ if player.isGrounded && ((forward != 0 ) || (sideway != 0 )) {
111+ headTimer += delta * 3.0
112+ walkLerp = rl.Lerp (walkLerp, 1.0 , 10.0 * delta)
113+ camera.fovy = rl.Lerp (camera.fovy, 55.0 , 5.0 * delta)
114+ } else {
115+ walkLerp = rl.Lerp (walkLerp, 0.0 , 10.0 * delta)
116+ camera.fovy = rl.Lerp (camera.fovy, 60.0 , 5.0 * delta)
117+ }
118+
119+ lean.x = rl.Lerp (lean.x, f32 (sideway) * 0.02 , 10.0 * delta)
120+ lean.y = rl.Lerp (lean.y, f32 (forward) * 0.015 , 10.0 * delta)
121+
122+ update_camera_fps (&camera)
123+ // ----------------------------------------------------------------------------------
124+
125+ // Draw
126+ // ----------------------------------------------------------------------------------
127+ rl.BeginDrawing ()
128+
129+ rl.ClearBackground (rl.RAYWHITE)
130+
131+ rl.BeginMode3D (camera)
132+ draw_level ()
133+ rl.EndMode3D ()
134+
135+ // Draw info box
136+ rl.DrawRectangle (5 , 5 , 330 , 75 , rl.Fade (rl.SKYBLUE, 0.5 ))
137+ rl.DrawRectangleLines (5 , 5 , 330 , 75 , rl.BLUE)
138+
139+ rl.DrawText (" Camera controls:" , 15 , 15 , 10 , rl.BLACK)
140+ rl.DrawText (" - Move keys: W, A, S, D, Space, Left-Ctrl" , 15 , 30 , 10 , rl.BLACK)
141+ rl.DrawText (" - Look around: arrow keys or mouse" , 15 , 45 , 10 , rl.BLACK)
142+ rl.DrawText (rl.TextFormat (" - Velocity Len: (%06.3f)" , rl.Vector2Length (player.velocity.xy)), 15 , 60 , 10 , rl.BLACK)
143+
144+ rl.EndDrawing ()
145+ // ----------------------------------------------------------------------------------
146+ }
147+
148+ // De-Initialization
149+ // --------------------------------------------------------------------------------------
150+ rl.CloseWindow () // Close window and OpenGL context
151+ // --------------------------------------------------------------------------------------
152+ }
153+
154+ // ----------------------------------------------------------------------------------
155+ // Module Functions Definition
156+ // ----------------------------------------------------------------------------------
157+ // Update body considering current world state
158+ update_body :: proc (body: ^Body, rot: f32 , side: i8 , forward: i8 , jumpPressed: bool , crouchHold: bool ) {
159+ input: rl.Vector2 = {f32 (side), f32 (-forward)}
160+
161+ if NORMALIZE_INPUT {
162+ // Slow down diagonal movement
163+ if (side != 0 ) && (forward != 0 ) {
164+ input = rl.Vector2Normalize (input)
165+ }
166+ }
167+
168+ delta := rl.GetFrameTime ()
169+
170+ if !body.isGrounded {
171+ body.velocity.y -= GRAVITY * delta
172+ }
173+
174+ if body.isGrounded && jumpPressed {
175+ body.velocity.y = JUMP_FORCE
176+ body.isGrounded = false
177+
178+ // Sound can be played at this moment
179+ // SetSoundPitch(fxJump, 1.0f + (GetRandomValue(-100, 100)*0.001));
180+ // PlaySound(fxJump);
181+ }
182+
183+ front := rl.Vector3{linalg.sin (rot), 0 , linalg.cos (rot)}
184+ right := rl.Vector3{linalg.cos (-rot), 0 , linalg.sin (-rot)}
185+
186+ desiredDir := rl.Vector3{input.x * right.x + input.y * front.x, 0.0 , input.x * right.z + input.y * front.z}
187+ body.dir = linalg.lerp (body.dir, desiredDir, CONTROL * delta)
188+
189+ decel : f32 = (body.isGrounded ? FRICTION : AIR_DRAG)
190+ hvel := rl.Vector3{body.velocity.x * decel, 0.0 , body.velocity.z * decel}
191+
192+ hvelLength := rl.Vector3Length (hvel) // Magnitude
193+ if hvelLength < (MAX_SPEED * 0.01 ) {
194+ hvel = rl.Vector3{0.0 , 0.0 , 0.0 }
195+ }
196+
197+ // This is what creates strafing
198+ speed := rl.Vector3DotProduct (hvel, body.dir)
199+
200+ // Whenever the amount of acceleration to add is clamped by the maximum acceleration constant,
201+ // a Player can make the speed faster by bringing the direction closer to horizontal velocity angle
202+ // More info here: https://youtu.be/v3zT3Z5apaM?t=165
203+ maxSpeed: f32 = (crouchHold ? CROUCH_SPEED : MAX_SPEED)
204+ accel := rl.Clamp (maxSpeed - speed, 0 , MAX_ACCEL * delta)
205+ hvel.xz += body.dir.xz * accel
206+
207+ body.velocity.xz = hvel.xz
208+
209+ body.position += body.velocity * delta
210+
211+ // Fancy collision system against the floor
212+ if body.position.y <= 0.0 {
213+ body.position.y = 0.0
214+ body.velocity.y = 0.0
215+ body.isGrounded = true // Enable jumping
216+ }
217+ }
218+
219+ // Update camera for FPS behaviour
220+ update_camera_fps :: proc (camera: ^rl.Camera) {
221+ UP :: rl.Vector3{0.0 , 1.0 , 0.0 }
222+ TARGET_OFFSET :: rl.Vector3{0.0 , 0.0 , -1.0 }
223+
224+ // Left and right
225+ yaw := rl.Vector3RotateByAxisAngle (TARGET_OFFSET, UP, lookRotation.x)
226+
227+ // Clamp view up
228+ maxAngleUp := rl.Vector3Angle (UP, yaw)
229+ maxAngleUp -= 0.001 // Avoid numerical errors
230+ if -lookRotation.y > maxAngleUp {
231+ lookRotation.y = -maxAngleUp
232+ }
233+
234+ // Clamp view down
235+ maxAngleDown := rl.Vector3Angle (-UP, yaw)
236+ maxAngleDown *= -1.0 // Downwards angle is negative
237+ maxAngleDown += 0.001 // Avoid numerical errors
238+ if -lookRotation.y < maxAngleDown {
239+ lookRotation.y = -maxAngleDown
240+ }
241+
242+ // Up and down
243+ right := rl.Vector3Normalize (rl.Vector3CrossProduct (yaw, UP))
244+
245+ // Rotate view vector around right axis
246+ pitchAngle := -lookRotation.y - lean.y
247+ pitchAngle = rl.Clamp (pitchAngle, -rl.PI / 2 + 0.0001 , rl.PI / 2 - 0.0001 ) // Clamp angle so it doesn't go past straight up or straight down
248+ pitch := rl.Vector3RotateByAxisAngle (yaw, right, pitchAngle)
249+
250+ // Head animation
251+ // Rotate up direction around forward axis
252+ headSin := linalg.sin (headTimer * rl.PI)
253+ headCos := linalg.cos (headTimer * rl.PI)
254+ STEP_ROTATION :: 0.01
255+ camera.up = rl.Vector3RotateByAxisAngle (UP, pitch, headSin * STEP_ROTATION + lean.x)
256+
257+ // Camera BOB
258+ BOB_SIDE :: 0.1
259+ BOB_UP :: 0.15
260+ bobbing := right * (headSin * BOB_SIDE)
261+ bobbing.y = abs (headCos * BOB_UP)
262+
263+ camera.position = camera.position + (bobbing * walkLerp)
264+ camera.target = camera.position + pitch
265+ }
266+
267+ // Draw game level
268+ draw_level :: proc () {
269+ FLOOR_EXTENT :: 25
270+ TILE_SIZE :: 5.0
271+ TILE_COLOR_1 :: rl.Color{150 , 200 , 200 , 255 }
272+
273+ // Floor tiles
274+ for y in -FLOOR_EXTENT..< FLOOR_EXTENT {
275+ for x in -FLOOR_EXTENT..< FLOOR_EXTENT {
276+ if (y & 1 != 0 ) && (x & 1 != 0 ) {
277+ rl.DrawPlane (rl.Vector3{f32 (x) * TILE_SIZE, 0.0 , f32 (y) * TILE_SIZE}, rl.Vector2{TILE_SIZE, TILE_SIZE}, TILE_COLOR_1)
278+ } else if (y & 1 == 0 ) && (x & 1 == 0 ) {
279+ rl.DrawPlane (rl.Vector3{f32 (x) * TILE_SIZE, 0.0 , f32 (y) * TILE_SIZE}, rl.Vector2{TILE_SIZE, TILE_SIZE}, rl.LIGHTGRAY)
280+ }
281+ }
282+ }
283+
284+ TOWER_SIZE :: rl.Vector3{16.0 , 32.0 , 16.0 }
285+ TOWER_COLOR :: rl.Color{150 , 200 , 200 , 255 }
286+
287+ towerPos := rl.Vector3{16.0 , 16.0 , 16.0 }
288+ rl.DrawCubeV (towerPos, TOWER_SIZE, TOWER_COLOR)
289+ rl.DrawCubeWiresV (towerPos, TOWER_SIZE, rl.DARKBLUE)
290+
291+ towerPos.x *= -1
292+ rl.DrawCubeV (towerPos, TOWER_SIZE, TOWER_COLOR)
293+ rl.DrawCubeWiresV (towerPos, TOWER_SIZE, rl.DARKBLUE)
294+
295+ towerPos.z *= -1
296+ rl.DrawCubeV (towerPos, TOWER_SIZE, TOWER_COLOR)
297+ rl.DrawCubeWiresV (towerPos, TOWER_SIZE, rl.DARKBLUE)
298+
299+ towerPos.x *= -1
300+ rl.DrawCubeV (towerPos, TOWER_SIZE, TOWER_COLOR)
301+ rl.DrawCubeWiresV (towerPos, TOWER_SIZE, rl.DARKBLUE)
302+
303+ // Red sun
304+ rl.DrawSphere ({300.0 , 300.0 , 0.0 }, 100 , rl.Color{255 , 0 , 0 , 255 })
305+ }
0 commit comments