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