Skip to content

Commit 13e384c

Browse files
authored
Merge pull request #5004 from nezvers/example_quake_controller
[examples] New example: `core_3d_fps_controller`
2 parents 9aa1b86 + ed022e8 commit 13e384c

File tree

5 files changed

+365
-0
lines changed

5 files changed

+365
-0
lines changed

examples/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@ CORE = \
507507
core/core_3d_camera_free \
508508
core/core_3d_camera_mode \
509509
core/core_3d_camera_split_screen \
510+
core/core_3d_fps_controller \
510511
core/core_3d_picking \
511512
core/core_automation_events \
512513
core/core_basic_screen_manager \

examples/Makefile.Web

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@ CORE = \
507507
core/core_3d_camera_free \
508508
core/core_3d_camera_mode \
509509
core/core_3d_camera_split_screen \
510+
core/core_3d_fps_controller \
510511
core/core_3d_picking \
511512
core/core_automation_events \
512513
core/core_basic_screen_manager \
@@ -702,6 +703,9 @@ core/core_3d_camera_mode: core/core_3d_camera_mode.c
702703
core/core_3d_camera_split_screen: core/core_3d_camera_split_screen.c
703704
$(CC) -o $@$(EXT) $< $(CFLAGS) $(INCLUDE_PATHS) $(LDFLAGS) $(LDLIBS) -D$(PLATFORM)
704705

706+
core/core_3d_fps_controller: core/core_3d_fps_controller.c
707+
$(CC) -o $@$(EXT) $< $(CFLAGS) $(INCLUDE_PATHS) $(LDFLAGS) $(LDLIBS) -D$(PLATFORM)
708+
705709
core/core_3d_picking: core/core_3d_picking.c
706710
$(CC) -o $@$(EXT) $< $(CFLAGS) $(INCLUDE_PATHS) $(LDFLAGS) $(LDLIBS) -D$(PLATFORM)
707711

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
/*******************************************************************************************
2+
*
3+
* raylib [core] example - Input Gestures for Web
4+
*
5+
* Example complexity rating: [★★★☆] 3/4
6+
*
7+
* Example originally created 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-2025 Agnis Aldins (@nezvers)
15+
*
16+
********************************************************************************************/
17+
18+
#include "raylib.h"
19+
#include "raymath.h"
20+
#include "rcamera.h"
21+
22+
#if defined(PLATFORM_WEB)
23+
#include <emscripten/emscripten.h>
24+
#endif
25+
26+
#if defined(PLATFORM_DESKTOP)
27+
#define GLSL_VERSION 330
28+
#else // PLATFORM_ANDROID, PLATFORM_WEB
29+
#define GLSL_VERSION 100
30+
#endif
31+
32+
33+
/* Movement constants */
34+
#define GRAVITY 32.f
35+
#define MAX_SPEED 20.f
36+
#define CROUCH_SPEED 5.f
37+
#define JUMP_FORCE 12.f
38+
#define MAX_ACCEL 150.f
39+
/* Grounded drag */
40+
#define FRICTION 0.86f
41+
/* Increasing air drag, increases strafing speed */
42+
#define AIR_DRAG 0.98f
43+
/* Responsiveness for turning movement direction to looked direction */
44+
#define CONTROL 15.f
45+
#define CROUCH_HEIGHT 0.f
46+
#define STAND_HEIGHT 1.f
47+
#define BOTTOM_HEIGHT 0.5f
48+
49+
#define NORMALIZE_INPUT 0
50+
51+
typedef struct {
52+
Vector3 position;
53+
Vector3 velocity;
54+
Vector3 dir;
55+
bool isGrounded;
56+
Sound soundJump;
57+
}Body;
58+
59+
const int screenWidth = 800;
60+
const int screenHeight = 450;
61+
Vector2 sensitivity = { 0.001f, 0.001f };
62+
63+
Body player;
64+
Camera camera;
65+
Vector2 lookRotation = { 0 };
66+
float headTimer;
67+
float walkLerp;
68+
float headLerp;
69+
Vector2 lean;
70+
71+
void UpdateDrawFrame(void); // Update and Draw one frame
72+
73+
void DrawLevel();
74+
75+
void UpdateCameraAngle();
76+
77+
void UpdateBody(Body* body, float rot, char side, char forward, bool jumpPressed, bool crouchHold);
78+
79+
//------------------------------------------------------------------------------------
80+
// Program main entry point
81+
//------------------------------------------------------------------------------------
82+
int main(void)
83+
{
84+
// Initialization
85+
//--------------------------------------------------------------------------------------
86+
InitWindow(screenWidth, screenHeight, "Raylib Quake-like controller");
87+
InitAudioDevice();
88+
89+
player = (Body){ Vector3Zero(), Vector3Zero(), Vector3Zero(), false, LoadSound("resources/huh_jump.wav")};
90+
camera = (Camera){ 0 };
91+
camera.fovy = 60.f; // Camera field-of-view Y
92+
camera.projection = CAMERA_PERSPECTIVE; // Camera projection type
93+
94+
lookRotation = Vector2Zero();
95+
headTimer = 0.f;
96+
walkLerp = 0.f;
97+
headLerp = STAND_HEIGHT;
98+
lean = Vector2Zero();
99+
100+
camera.position = (Vector3){
101+
player.position.x,
102+
player.position.y + (BOTTOM_HEIGHT + headLerp),
103+
player.position.z,
104+
};
105+
UpdateCameraAngle();
106+
107+
DisableCursor(); // Limit cursor to relative movement inside the window
108+
109+
#if defined(PLATFORM_WEB)
110+
emscripten_set_main_loop(UpdateDrawFrame, 0, 1);
111+
#else
112+
113+
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
114+
//--------------------------------------------------------------------------------------
115+
116+
// Main game loop
117+
while (!WindowShouldClose()) // Detect window close button or ESC key
118+
{
119+
UpdateDrawFrame();
120+
}
121+
#endif
122+
123+
// De-Initialization
124+
//--------------------------------------------------------------------------------------
125+
UnloadSound(player.soundJump);
126+
CloseAudioDevice();
127+
CloseWindow(); // Close window and OpenGL context
128+
//--------------------------------------------------------------------------------------
129+
130+
return 0;
131+
}
132+
133+
void UpdateDrawFrame(void)
134+
{
135+
// Update
136+
//----------------------------------------------------------------------------------
137+
138+
Vector2 mouse_delta = GetMouseDelta();
139+
lookRotation.x -= mouse_delta.x * sensitivity.x;
140+
lookRotation.y += mouse_delta.y * sensitivity.y;
141+
142+
char sideway = (IsKeyDown(KEY_D) - IsKeyDown(KEY_A));
143+
char forward = (IsKeyDown(KEY_W) - IsKeyDown(KEY_S));
144+
bool crouching = IsKeyDown(KEY_LEFT_CONTROL);
145+
UpdateBody(&player, lookRotation.x, sideway, forward, IsKeyPressed(KEY_SPACE), crouching);
146+
147+
float delta = GetFrameTime();
148+
headLerp = Lerp(headLerp, (crouching ? CROUCH_HEIGHT : STAND_HEIGHT), 20.f * delta);
149+
camera.position = (Vector3){
150+
player.position.x,
151+
player.position.y + (BOTTOM_HEIGHT + headLerp),
152+
player.position.z,
153+
};
154+
155+
if (player.isGrounded && (forward != 0 || sideway != 0)) {
156+
headTimer += delta * 3.f;
157+
walkLerp = Lerp(walkLerp, 1.f, 10.f * delta);
158+
camera.fovy = Lerp(camera.fovy, 55.f, 5.f * delta);
159+
}
160+
else {
161+
walkLerp = Lerp(walkLerp, 0.f, 10.f * delta);
162+
camera.fovy = Lerp(camera.fovy, 60.f, 5.f * delta);
163+
}
164+
165+
lean.x = Lerp(lean.x, sideway * 0.02f, 10.f * delta);
166+
lean.y = Lerp(lean.y, forward * 0.015f, 10.f * delta);
167+
168+
UpdateCameraAngle();
169+
170+
// Draw
171+
//----------------------------------------------------------------------------------
172+
BeginDrawing();
173+
174+
ClearBackground(RAYWHITE);
175+
176+
BeginMode3D(camera);
177+
178+
DrawLevel();
179+
180+
EndMode3D();
181+
182+
// Draw info box
183+
DrawRectangle(5, 5, 330, 100, Fade(SKYBLUE, 0.5f));
184+
DrawRectangleLines(5, 5, 330, 100, BLUE);
185+
186+
DrawText("Camera controls:", 15, 15, 10, BLACK);
187+
DrawText("- Move keys: W, A, S, D, Space, Left-Ctrl", 15, 30, 10, BLACK);
188+
DrawText("- Look around: arrow keys or mouse", 15, 45, 10, BLACK);
189+
DrawText(TextFormat("- Velocity Len: (%06.3f)", Vector2Length((Vector2) { player.velocity.x, player.velocity.z })), 15, 60, 10, BLACK);
190+
191+
192+
EndDrawing();
193+
//----------------------------------------------------------------------------------
194+
}
195+
196+
void UpdateBody(Body* body, float rot, char side, char forward, bool jumpPressed, bool crouchHold)
197+
{
198+
Vector2 input = (Vector2){ (float)side, (float)-forward };
199+
#if defined(NORMALIZE_INPUT)
200+
// Slow down diagonal movement
201+
if (side != 0 & forward != 0)
202+
{
203+
input = Vector2Normalize(input);
204+
}
205+
#endif
206+
207+
float delta = GetFrameTime();
208+
209+
if (!body->isGrounded)
210+
{
211+
body->velocity.y -= GRAVITY * delta;
212+
}
213+
if (body->isGrounded && jumpPressed)
214+
{
215+
body->velocity.y = JUMP_FORCE;
216+
body->isGrounded = false;
217+
SetSoundPitch(body->soundJump, 1.f + (GetRandomValue(-100, 100) * 0.001));
218+
PlaySound(body->soundJump);
219+
}
220+
221+
Vector3 front_vec = (Vector3){ sin(rot), 0.f, cos(rot) };
222+
Vector3 right_vec = (Vector3){ cos(-rot), 0.f, sin(-rot) };
223+
224+
Vector3 desired_dir = (Vector3){
225+
input.x * right_vec.x + input.y * front_vec.x,
226+
0.f,
227+
input.x * right_vec.z + input.y * front_vec.z,
228+
};
229+
230+
body->dir = Vector3Lerp(body->dir, desired_dir, CONTROL * delta);
231+
232+
float decel = body->isGrounded ? FRICTION : AIR_DRAG;
233+
Vector3 hvel = (Vector3){
234+
body->velocity.x * decel,
235+
0.f,
236+
body->velocity.z * decel
237+
};
238+
239+
float hvel_length = Vector3Length(hvel); // a.k.a. magnitude
240+
if (hvel_length < MAX_SPEED * 0.01f) {
241+
hvel = (Vector3){ 0 };
242+
}
243+
244+
/* This is what creates strafing */
245+
float speed = Vector3DotProduct(hvel, body->dir);
246+
247+
/*
248+
Whenever the amount of acceleration to add is clamped by the maximum acceleration constant,
249+
a Player can make the speed faster by bringing the direction closer to horizontal velocity angle
250+
More info here: https://youtu.be/v3zT3Z5apaM?t=165
251+
*/
252+
float max_speed = crouchHold ? CROUCH_SPEED : MAX_SPEED;
253+
float accel = Clamp(max_speed - speed, 0.f, MAX_ACCEL * delta);
254+
hvel.x += body->dir.x * accel;
255+
hvel.z += body->dir.z * accel;
256+
257+
body->velocity.x = hvel.x;
258+
body->velocity.z = hvel.z;
259+
260+
body->position.x += body->velocity.x * delta;
261+
body->position.y += body->velocity.y * delta;
262+
body->position.z += body->velocity.z * delta;
263+
264+
/* Fancy collision system against "THE FLOOR" */
265+
if (body->position.y <= 0.f)
266+
{
267+
body->position.y = 0.f;
268+
body->velocity.y = 0.f;
269+
body->isGrounded = true; // <= enables jumping
270+
}
271+
}
272+
273+
void UpdateCameraAngle()
274+
{
275+
const Vector3 up = (Vector3){ 0.f, 1.f, 0.f };
276+
const Vector3 targetOffset = (Vector3){ 0.f, 0.f, -1.f };
277+
278+
/* Left & Right */
279+
Vector3 yaw = Vector3RotateByAxisAngle(targetOffset, up, lookRotation.x);
280+
281+
// Clamp view up
282+
float maxAngleUp = Vector3Angle(up, yaw);
283+
maxAngleUp -= 0.001f; // avoid numerical errors
284+
if ( -(lookRotation.y) > maxAngleUp) { lookRotation.y = -maxAngleUp; }
285+
286+
// Clamp view down
287+
float maxAngleDown = Vector3Angle(Vector3Negate(up), yaw);
288+
maxAngleDown *= -1.0f; // downwards angle is negative
289+
maxAngleDown += 0.001f; // avoid numerical errors
290+
if ( -(lookRotation.y) < maxAngleDown) { lookRotation.y = -maxAngleDown; }
291+
292+
/* Up & Down */
293+
Vector3 right = Vector3Normalize(Vector3CrossProduct(yaw, up));
294+
295+
// Rotate view vector around right axis
296+
Vector3 pitch = Vector3RotateByAxisAngle(yaw, right, -lookRotation.y - lean.y);
297+
298+
// Head animation
299+
// Rotate up direction around forward axis
300+
float _sin = sin(headTimer * PI);
301+
float _cos = cos(headTimer * PI);
302+
const float stepRotation = 0.01f;
303+
camera.up = Vector3RotateByAxisAngle(up, pitch, _sin * stepRotation + lean.x);
304+
305+
/* BOB */
306+
const float bobSide = 0.1f;
307+
const float bobUp = 0.15f;
308+
Vector3 bobbing = Vector3Scale(right, _sin * bobSide);
309+
bobbing.y = fabsf(_cos * bobUp);
310+
camera.position = Vector3Add(camera.position, Vector3Scale(bobbing, walkLerp));
311+
312+
camera.target = Vector3Add(camera.position, pitch);
313+
}
314+
315+
316+
void DrawLevel()
317+
{
318+
const int floorExtent = 25;
319+
const float tileSize = 5.f;
320+
const Color tileColor1 = (Color){ 150, 200, 200, 255 };
321+
// Floor tiles
322+
for (int y = -floorExtent; y < floorExtent; y++)
323+
{
324+
for (int x = -floorExtent; x < floorExtent; x++)
325+
{
326+
if ((y & 1) && (x & 1))
327+
{
328+
DrawPlane((Vector3) { x * tileSize, 0.f, y * tileSize},
329+
(Vector2) {tileSize, tileSize}, tileColor1);
330+
}
331+
else if(!(y & 1) && !(x & 1))
332+
{
333+
DrawPlane((Vector3) { x * tileSize, 0.f, y * tileSize},
334+
(Vector2) {tileSize, tileSize}, LIGHTGRAY);
335+
}
336+
}
337+
}
338+
339+
const Vector3 towerSize = (Vector3){ 16.f, 32.f, 16.f };
340+
const Color towerColor = (Color){ 150, 200, 200, 255 };
341+
342+
Vector3 towerPos = (Vector3){ 16.f, 16.f, 16.f };
343+
DrawCubeV(towerPos, towerSize, towerColor);
344+
DrawCubeWiresV(towerPos, towerSize, DARKBLUE);
345+
346+
towerPos.x *= -1;
347+
DrawCubeV(towerPos, towerSize, towerColor);
348+
DrawCubeWiresV(towerPos, towerSize, DARKBLUE);
349+
350+
towerPos.z *= -1;
351+
DrawCubeV(towerPos, towerSize, towerColor);
352+
DrawCubeWiresV(towerPos, towerSize, DARKBLUE);
353+
354+
towerPos.x *= -1;
355+
DrawCubeV(towerPos, towerSize, towerColor);
356+
DrawCubeWiresV(towerPos, towerSize, DARKBLUE);
357+
358+
// Red sun
359+
DrawSphere((Vector3) { 300.f, 300.f, 0.f }, 100.f, (Color) { 255, 0, 0, 255 });
360+
}
8.87 KB
Loading

examples/core/resources/huh_jump.wav

18.6 KB
Binary file not shown.

0 commit comments

Comments
 (0)