raylib/ports/core/core_3d_camera_fps
Licence This example has its own licence, rather than Odin's: raylib/ports/LICENSE
These are ported Raylib examples, and as such, they are subject to Raylib's license.
Copyright (c) 2013-2024 Ramon Santamaria (@raysan5)
The rest of the licence
This software is provided "as-is", without any express or implied warranty. In no event
will the authors be held liable for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any purpose, including commercial
applications, and to alter it and redistribute it freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not claim that you
wrote the original software. If you use this software in a product, an acknowledgment
in the product documentation would be appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be misrepresented
as being the original software.
3. This notice may not be removed or altered from any source distribution.
Code
core_3d_camera_fps.odin ¶305 linesSource
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
18package raylib_examples
19
20import "core:math/linalg"
21import rl "vendor:raylib"
22
23//----------------------------------------------------------------------------------
24// Defines and Macros
25//----------------------------------------------------------------------------------
26// Movement constants
27GRAVITY :: 32.0
28MAX_SPEED :: 20.0
29CROUCH_SPEED :: 5.0
30JUMP_FORCE :: 12.0
31MAX_ACCEL :: 150.0
32// Grounded drag
33FRICTION :: 0.86
34// Increasing air drag, increases strafing speed
35AIR_DRAG :: 0.98
36// Responsiveness for turning movement direction to looked direction
37CONTROL :: 15.0
38CROUCH_HEIGHT :: 0.0
39STAND_HEIGHT :: 1.0
40BOTTOM_HEIGHT :: 0.5
41
42NORMALIZE_INPUT :: false
43
44//----------------------------------------------------------------------------------
45// Types and Structures Definition
46//----------------------------------------------------------------------------------
47// Body structure
48Body :: struct {
49 position: rl.Vector3,
50 velocity: rl.Vector3,
51 dir: rl.Vector3,
52 isGrounded: bool,
53}
54
55//----------------------------------------------------------------------------------
56// Global Variables Definition
57//----------------------------------------------------------------------------------
58sensitivity := rl.Vector2{0.001, 0.001}
59
60player: Body
61lookRotation: rl.Vector2
62headTimer: f32
63walkLerp: f32
64headLerp: f32 = STAND_HEIGHT
65lean: rl.Vector2
66
67//------------------------------------------------------------------------------------
68// Program main entry point
69//------------------------------------------------------------------------------------
70main :: 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
158update_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
220update_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
268draw_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}
Declarations Used 47
- core:math/linalg cos · lerp · sin
- vendor:raylib BeginDrawing · BeginMode3D · BLACK · BLUE · Camera · Clamp · ClearBackground · CloseWindow · Color · DARKBLUE · DisableCursor · DrawCubeV · DrawCubeWiresV · DrawPlane · DrawRectangle · DrawRectangleLines · DrawSphere · DrawText · EndDrawing · EndMode3D · Fade · GetFrameTime · GetMouseDelta · InitWindow · IsKeyDown · IsKeyPressed · Lerp · LIGHTGRAY · PI · RAYWHITE · SetTargetFPS · SKYBLUE · TextFormat · Vector2 · Vector2Length · Vector2Normalize · Vector3 · Vector3Angle · Vector3CrossProduct · Vector3DotProduct · Vector3Length · Vector3Normalize · Vector3RotateByAxisAngle · WindowShouldClose