Skip to content

[example] New example: core_3d_fps_controller #5004

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ CORE = \
core/core_3d_camera_free \
core/core_3d_camera_mode \
core/core_3d_camera_split_screen \
core/core_3d_fps_controller \
core/core_3d_picking \
core/core_automation_events \
core/core_basic_screen_manager \
Expand Down
4 changes: 4 additions & 0 deletions examples/Makefile.Web
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ CORE = \
core/core_3d_camera_free \
core/core_3d_camera_mode \
core/core_3d_camera_split_screen \
core/core_3d_fps_controller \
core/core_3d_picking \
core/core_automation_events \
core/core_basic_screen_manager \
Expand Down Expand Up @@ -587,6 +588,9 @@ core/core_3d_camera_mode: core/core_3d_camera_mode.c
core/core_3d_camera_split_screen: core/core_3d_camera_split_screen.c
$(CC) -o $@$(EXT) $< $(CFLAGS) $(INCLUDE_PATHS) $(LDFLAGS) $(LDLIBS) -D$(PLATFORM)

core/core_3d_fps_controller: core/core_3d_fps_controller.c
$(CC) -o $@$(EXT) $< $(CFLAGS) $(INCLUDE_PATHS) $(LDFLAGS) $(LDLIBS) -D$(PLATFORM)

core/core_3d_picking: core/core_3d_picking.c
$(CC) -o $@$(EXT) $< $(CFLAGS) $(INCLUDE_PATHS) $(LDFLAGS) $(LDLIBS) -D$(PLATFORM)

Expand Down
360 changes: 360 additions & 0 deletions examples/core/core_3d_fps_controller.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
/*******************************************************************************************
*
* raylib [core] example - Input Gestures for Web
*
* Example complexity rating: [★★★☆] 3/4
*
* Example originally created with raylib 5.5
*
* Example contributed by Agnis Aldins (@nezvers) and reviewed by Ramon Santamaria (@raysan5)
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2025-2025 Agnis Aldins (@nezvers)
*
********************************************************************************************/

#include "raylib.h"
#include "raymath.h"
#include "rcamera.h"

#if defined(PLATFORM_WEB)
#include <emscripten/emscripten.h>
#endif

#if defined(PLATFORM_DESKTOP)
#define GLSL_VERSION 330
#else // PLATFORM_ANDROID, PLATFORM_WEB
#define GLSL_VERSION 100
#endif


/* Movement constants */
#define GRAVITY 32.f
#define MAX_SPEED 20.f
#define CROUCH_SPEED 5.f
#define JUMP_FORCE 12.f
#define MAX_ACCEL 150.f
/* Grounded drag */
#define FRICTION 0.86f
/* Increasing air drag, increases strafing speed */
#define AIR_DRAG 0.98f
/* Responsiveness for turning movement direction to looked direction */
#define CONTROL 15.f
#define CROUCH_HEIGHT 0.f
#define STAND_HEIGHT 1.f
#define BOTTOM_HEIGHT 0.5f

#define NORMALIZE_INPUT 0

typedef struct {
Vector3 position;
Vector3 velocity;
Vector3 dir;
bool isGrounded;
Sound soundJump;
}Body;

const int screenWidth = 800;
const int screenHeight = 450;
Vector2 sensitivity = { 0.001f, 0.001f };

Body player;
Camera camera;
Vector2 lookRotation = { 0 };
float headTimer;
float walkLerp;
float headLerp;
Vector2 lean;

void UpdateDrawFrame(void); // Update and Draw one frame

void DrawLevel();

void UpdateCameraAngle();

void UpdateBody(Body* body, float rot, char side, char forward, bool jumpPressed, bool crouchHold);

//------------------------------------------------------------------------------------
// Program main entry point
//------------------------------------------------------------------------------------
int main(void)
{
// Initialization
//--------------------------------------------------------------------------------------
InitWindow(screenWidth, screenHeight, "Raylib Quake-like controller");
InitAudioDevice();

player = (Body){ Vector3Zero(), Vector3Zero(), Vector3Zero(), false, LoadSound("resources/huh_jump.wav")};
camera = (Camera){ 0 };
camera.fovy = 60.f; // Camera field-of-view Y
camera.projection = CAMERA_PERSPECTIVE; // Camera projection type

lookRotation = Vector2Zero();
headTimer = 0.f;
walkLerp = 0.f;
headLerp = STAND_HEIGHT;
lean = Vector2Zero();

camera.position = (Vector3){
player.position.x,
player.position.y + (BOTTOM_HEIGHT + headLerp),
player.position.z,
};
UpdateCameraAngle();

DisableCursor(); // Limit cursor to relative movement inside the window

#if defined(PLATFORM_WEB)
emscripten_set_main_loop(UpdateDrawFrame, 0, 1);
#else

SetTargetFPS(60); // Set our game to run at 60 frames-per-second
//--------------------------------------------------------------------------------------

// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
UpdateDrawFrame();
}
#endif

// De-Initialization
//--------------------------------------------------------------------------------------
UnloadSound(player.soundJump);
CloseAudioDevice();
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------

return 0;
}

void UpdateDrawFrame(void)
{
// Update
//----------------------------------------------------------------------------------

Vector2 mouse_delta = GetMouseDelta();
lookRotation.x -= mouse_delta.x * sensitivity.x;
lookRotation.y += mouse_delta.y * sensitivity.y;

char sideway = (IsKeyDown(KEY_D) - IsKeyDown(KEY_A));
char forward = (IsKeyDown(KEY_W) - IsKeyDown(KEY_S));
bool crouching = IsKeyDown(KEY_LEFT_CONTROL);
UpdateBody(&player, lookRotation.x, sideway, forward, IsKeyPressed(KEY_SPACE), crouching);

float delta = GetFrameTime();
headLerp = Lerp(headLerp, (crouching ? CROUCH_HEIGHT : STAND_HEIGHT), 20.f * delta);
camera.position = (Vector3){
player.position.x,
player.position.y + (BOTTOM_HEIGHT + headLerp),
player.position.z,
};

if (player.isGrounded && (forward != 0 || sideway != 0)) {
headTimer += delta * 3.f;
walkLerp = Lerp(walkLerp, 1.f, 10.f * delta);
camera.fovy = Lerp(camera.fovy, 55.f, 5.f * delta);
}
else {
walkLerp = Lerp(walkLerp, 0.f, 10.f * delta);
camera.fovy = Lerp(camera.fovy, 60.f, 5.f * delta);
}

lean.x = Lerp(lean.x, sideway * 0.02f, 10.f * delta);
lean.y = Lerp(lean.y, forward * 0.015f, 10.f * delta);

UpdateCameraAngle();

// Draw
//----------------------------------------------------------------------------------
BeginDrawing();

ClearBackground(RAYWHITE);

BeginMode3D(camera);

DrawLevel();

EndMode3D();

// Draw info box
DrawRectangle(5, 5, 330, 100, Fade(SKYBLUE, 0.5f));
DrawRectangleLines(5, 5, 330, 100, BLUE);

DrawText("Camera controls:", 15, 15, 10, BLACK);
DrawText("- Move keys: W, A, S, D, Space, Left-Ctrl", 15, 30, 10, BLACK);
DrawText("- Look around: arrow keys or mouse", 15, 45, 10, BLACK);
DrawText(TextFormat("- Velocity Len: (%06.3f)", Vector2Length((Vector2) { player.velocity.x, player.velocity.z })), 15, 60, 10, BLACK);


EndDrawing();
//----------------------------------------------------------------------------------
}

void UpdateBody(Body* body, float rot, char side, char forward, bool jumpPressed, bool crouchHold)
{
Vector2 input = (Vector2){ (float)side, (float)-forward };
#if defined(NORMALIZE_INPUT)
// Slow down diagonal movement
if (side != 0 & forward != 0)
{
input = Vector2Normalize(input);
}
#endif

float delta = GetFrameTime();

if (!body->isGrounded)
{
body->velocity.y -= GRAVITY * delta;
}
if (body->isGrounded && jumpPressed)
{
body->velocity.y = JUMP_FORCE;
body->isGrounded = false;
SetSoundPitch(body->soundJump, 1.f + (GetRandomValue(-100, 100) * 0.001));
PlaySound(body->soundJump);
}

Vector3 front_vec = (Vector3){ sin(rot), 0.f, cos(rot) };
Vector3 right_vec = (Vector3){ cos(-rot), 0.f, sin(-rot) };

Vector3 desired_dir = (Vector3){
input.x * right_vec.x + input.y * front_vec.x,
0.f,
input.x * right_vec.z + input.y * front_vec.z,
};

body->dir = Vector3Lerp(body->dir, desired_dir, CONTROL * delta);

float decel = body->isGrounded ? FRICTION : AIR_DRAG;
Vector3 hvel = (Vector3){
body->velocity.x * decel,
0.f,
body->velocity.z * decel
};

float hvel_length = Vector3Length(hvel); // a.k.a. magnitude
if (hvel_length < MAX_SPEED * 0.01f) {
hvel = (Vector3){ 0 };
}

/* This is what creates strafing */
float speed = Vector3DotProduct(hvel, body->dir);

/*
Whenever the amount of acceleration to add is clamped by the maximum acceleration constant,
a Player can make the speed faster by bringing the direction closer to horizontal velocity angle
More info here: https://youtu.be/v3zT3Z5apaM?t=165
*/
float max_speed = crouchHold ? CROUCH_SPEED : MAX_SPEED;
float accel = Clamp(max_speed - speed, 0.f, MAX_ACCEL * delta);
hvel.x += body->dir.x * accel;
hvel.z += body->dir.z * accel;

body->velocity.x = hvel.x;
body->velocity.z = hvel.z;

body->position.x += body->velocity.x * delta;
body->position.y += body->velocity.y * delta;
body->position.z += body->velocity.z * delta;

/* Fancy collision system against "THE FLOOR" */
if (body->position.y <= 0.f)
{
body->position.y = 0.f;
body->velocity.y = 0.f;
body->isGrounded = true; // <= enables jumping
}
}

void UpdateCameraAngle()
{
const Vector3 up = (Vector3){ 0.f, 1.f, 0.f };
const Vector3 targetOffset = (Vector3){ 0.f, 0.f, -1.f };

/* Left & Right */
Vector3 yaw = Vector3RotateByAxisAngle(targetOffset, up, lookRotation.x);

// Clamp view up
float maxAngleUp = Vector3Angle(up, yaw);
maxAngleUp -= 0.001f; // avoid numerical errors
if ( -(lookRotation.y) > maxAngleUp) { lookRotation.y = -maxAngleUp; }

// Clamp view down
float maxAngleDown = Vector3Angle(Vector3Negate(up), yaw);
maxAngleDown *= -1.0f; // downwards angle is negative
maxAngleDown += 0.001f; // avoid numerical errors
if ( -(lookRotation.y) < maxAngleDown) { lookRotation.y = -maxAngleDown; }

/* Up & Down */
Vector3 right = Vector3Normalize(Vector3CrossProduct(yaw, up));

// Rotate view vector around right axis
Vector3 pitch = Vector3RotateByAxisAngle(yaw, right, -lookRotation.y - lean.y);

// Head animation
// Rotate up direction around forward axis
float _sin = sin(headTimer * PI);
float _cos = cos(headTimer * PI);
const float stepRotation = 0.01f;
camera.up = Vector3RotateByAxisAngle(up, pitch, _sin * stepRotation + lean.x);

/* BOB */
const float bobSide = 0.1f;
const float bobUp = 0.15f;
Vector3 bobbing = Vector3Scale(right, _sin * bobSide);
bobbing.y = fabsf(_cos * bobUp);
camera.position = Vector3Add(camera.position, Vector3Scale(bobbing, walkLerp));

camera.target = Vector3Add(camera.position, pitch);
}


void DrawLevel()
{
const int floorExtent = 25;
const float tileSize = 5.f;
const Color tileColor1 = (Color){ 150, 200, 200, 255 };
// Floor tiles
for (int y = -floorExtent; y < floorExtent; y++)
{
for (int x = -floorExtent; x < floorExtent; x++)
{
if ((y & 1) && (x & 1))
{
DrawPlane((Vector3) { x * tileSize, 0.f, y * tileSize},
(Vector2) {tileSize, tileSize}, tileColor1);
}
else if(!(y & 1) && !(x & 1))
{
DrawPlane((Vector3) { x * tileSize, 0.f, y * tileSize},
(Vector2) {tileSize, tileSize}, LIGHTGRAY);
}
}
}

const Vector3 towerSize = (Vector3){ 16.f, 32.f, 16.f };
const Color towerColor = (Color){ 150, 200, 200, 255 };

Vector3 towerPos = (Vector3){ 16.f, 16.f, 16.f };
DrawCubeV(towerPos, towerSize, towerColor);
DrawCubeWiresV(towerPos, towerSize, DARKBLUE);

towerPos.x *= -1;
DrawCubeV(towerPos, towerSize, towerColor);
DrawCubeWiresV(towerPos, towerSize, DARKBLUE);

towerPos.z *= -1;
DrawCubeV(towerPos, towerSize, towerColor);
DrawCubeWiresV(towerPos, towerSize, DARKBLUE);

towerPos.x *= -1;
DrawCubeV(towerPos, towerSize, towerColor);
DrawCubeWiresV(towerPos, towerSize, DARKBLUE);

// Red sun
DrawSphere((Vector3) { 300.f, 300.f, 0.f }, 100.f, (Color) { 255, 0, 0, 255 });
}
Binary file added examples/core/core_3d_fps_controller.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/core/resources/huh_jump.wav
Binary file not shown.
Loading