|
| 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 | +} |
0 commit comments