Skip to content

Commit 16afa5e

Browse files
authored
Added core_3d_camera_fps.odin example (#129)
* Added core_3d_camera_fps.odin example * Removed deprecated comment * Refactored code based on feedback, naming and style conventions * Updated code to follow naming, style conventions and odin features * Updated variable operations to use swizzling feature Also improved code formatting
1 parent e4ab9d0 commit 16afa5e

File tree

2 files changed

+306
-0
lines changed

2 files changed

+306
-0
lines changed

.github/workflows/check.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ jobs:
7676
odin check raylib/ports/textures/textures_gif_player.odin -file $FLAGS
7777
odin check raylib/tetroid $FLAGS
7878
odin check raylib/box2d $FLAGS
79+
odin check raylib/ports/core/core_3d_camera_fps.odin -file $FLAGS
7980
odin check raylib/ports/core/core_basic_window.odin -file $FLAGS
8081
8182
odin check directx/d3d11_minimal_sdl2 -target:windows_amd64 $FLAGS
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
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

Comments
 (0)