360 lines
13 KiB
C#
360 lines
13 KiB
C#
/*******************************************************************************************
|
|
*
|
|
* raylib [core] example - 3d camera fps
|
|
*
|
|
* Example complexity rating: [★★★☆] 3/4
|
|
*
|
|
* Example originally created with raylib 5.5, last time updated with raylib 5.5
|
|
*
|
|
* Example contributed by Agnis Aldiņš (@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 Agnis Aldiņš (@nezvers)
|
|
*
|
|
********************************************************************************************/
|
|
|
|
using static Raylib_cs.Raymath;
|
|
|
|
namespace Examples.Core;
|
|
|
|
public partial class Camera3dFps : IExample
|
|
{
|
|
// Movement constants
|
|
private const float GRAVITY = 32.0f;
|
|
private const float MAX_SPEED = 20.0f;
|
|
private const float CROUCH_SPEED = 5.0f;
|
|
private const float JUMP_FORCE = 12.0f;
|
|
private const float MAX_ACCEL = 150.0f;
|
|
// Grounded drag
|
|
private const float FRICTION = 0.86f;
|
|
// Increasing air drag, increases strafing speed
|
|
private const float AIR_DRAG = 0.98f;
|
|
// Responsiveness for turning movement direction to looked direction
|
|
private const float CONTROL = 15.0f;
|
|
private const float CROUCH_HEIGHT = 0.0f;
|
|
private const float STAND_HEIGHT = 1.0f;
|
|
private const float BOTTOM_HEIGHT = 0.5f;
|
|
|
|
private const int screenWidth = 800;
|
|
private const int screenHeight = 450;
|
|
|
|
public string Name => "Core / 3D Camera FPS";
|
|
|
|
public string Title => "raylib [core] example - 3d camera fps";
|
|
|
|
public bool CursorDisabled => true;
|
|
|
|
// Body structure
|
|
private struct Body
|
|
{
|
|
public Vector3 Position;
|
|
public Vector3 Velocity;
|
|
public Vector3 Dir;
|
|
public bool IsGrounded;
|
|
}
|
|
|
|
// State that was global in the C example
|
|
private readonly Vector2 sensitivity = new Vector2(0.001f, 0.001f);
|
|
|
|
private Body player;
|
|
private Vector2 lookRotation;
|
|
private float headTimer;
|
|
private float walkLerp;
|
|
private float headLerp;
|
|
private Vector2 lean;
|
|
|
|
private Camera3D camera;
|
|
|
|
public void Init()
|
|
{
|
|
player = new Body();
|
|
lookRotation = new Vector2(0, 0);
|
|
headTimer = 0.0f;
|
|
walkLerp = 0.0f;
|
|
headLerp = STAND_HEIGHT;
|
|
lean = new Vector2(0, 0);
|
|
|
|
// Initialize camera variables
|
|
// NOTE: UpdateCameraFPS() takes care of the rest
|
|
camera = new Camera3D();
|
|
camera.FovY = 60.0f;
|
|
camera.Projection = CameraProjection.Perspective;
|
|
camera.Position = new Vector3(
|
|
player.Position.X,
|
|
player.Position.Y + (BOTTOM_HEIGHT + headLerp),
|
|
player.Position.Z);
|
|
|
|
UpdateCameraFPS(ref camera); // Update camera parameters
|
|
}
|
|
|
|
public void Update()
|
|
{
|
|
// Update
|
|
//----------------------------------------------------------------------------------
|
|
Vector2 mouseDelta = GetMouseDelta();
|
|
lookRotation.X -= mouseDelta.X * sensitivity.X;
|
|
lookRotation.Y += mouseDelta.Y * sensitivity.Y;
|
|
|
|
int sideway = (IsKeyDown(KeyboardKey.D) ? 1 : 0) - (IsKeyDown(KeyboardKey.A) ? 1 : 0);
|
|
int forward = (IsKeyDown(KeyboardKey.W) ? 1 : 0) - (IsKeyDown(KeyboardKey.S) ? 1 : 0);
|
|
bool crouching = IsKeyDown(KeyboardKey.LeftControl);
|
|
UpdateBody(ref player, lookRotation.X, sideway, forward, IsKeyPressed(KeyboardKey.Space), crouching);
|
|
|
|
float delta = GetFrameTime();
|
|
headLerp = Lerp(headLerp, (crouching ? CROUCH_HEIGHT : STAND_HEIGHT), 20.0f * delta);
|
|
camera.Position = new Vector3(
|
|
player.Position.X,
|
|
player.Position.Y + (BOTTOM_HEIGHT + headLerp),
|
|
player.Position.Z);
|
|
|
|
if (player.IsGrounded && ((forward != 0) || (sideway != 0)))
|
|
{
|
|
headTimer += delta * 3.0f;
|
|
walkLerp = Lerp(walkLerp, 1.0f, 10.0f * delta);
|
|
camera.FovY = Lerp(camera.FovY, 55.0f, 5.0f * delta);
|
|
}
|
|
else
|
|
{
|
|
walkLerp = Lerp(walkLerp, 0.0f, 10.0f * delta);
|
|
camera.FovY = Lerp(camera.FovY, 60.0f, 5.0f * delta);
|
|
}
|
|
|
|
lean.X = Lerp(lean.X, sideway * 0.02f, 10.0f * delta);
|
|
lean.Y = Lerp(lean.Y, forward * 0.015f, 10.0f * delta);
|
|
|
|
UpdateCameraFPS(ref camera);
|
|
//----------------------------------------------------------------------------------
|
|
|
|
// Draw
|
|
//----------------------------------------------------------------------------------
|
|
BeginDrawing();
|
|
|
|
ClearBackground(Color.RayWhite);
|
|
|
|
BeginMode3D(camera);
|
|
DrawLevel();
|
|
EndMode3D();
|
|
|
|
// Draw info box
|
|
DrawRectangle(5, 5, 330, 75, Fade(Color.SkyBlue, 0.5f));
|
|
DrawRectangleLines(5, 5, 330, 75, Color.Blue);
|
|
|
|
DrawText("Camera controls:", 15, 15, 10, Color.Black);
|
|
DrawText("- Move keys: W, A, S, D, Space, Left-Ctrl", 15, 30, 10, Color.Black);
|
|
DrawText("- Look around: arrow keys or mouse", 15, 45, 10, Color.Black);
|
|
float velLen = Vector2Length(new Vector2(player.Velocity.X, player.Velocity.Z));
|
|
DrawText($"- Velocity Len: ({velLen:00.000})", 15, 60, 10, Color.Black);
|
|
|
|
EndDrawing();
|
|
//----------------------------------------------------------------------------------
|
|
}
|
|
|
|
public void Unload()
|
|
{
|
|
}
|
|
|
|
// Update body considering current world state
|
|
private void UpdateBody(ref Body body, float rot, int side, int forward, bool jumpPressed, bool crouchHold)
|
|
{
|
|
Vector2 input = new Vector2((float)side, (float)-forward);
|
|
|
|
// Upstream guards this with `#if defined(NORMALIZE_INPUT)`, which is always true given the
|
|
// `#define NORMALIZE_INPUT 0` above it (defined() tests definedness, not the value), so the
|
|
// diagonal-movement normalization is active.
|
|
// Slow down diagonal movement
|
|
if ((side != 0) && (forward != 0))
|
|
{
|
|
input = Vector2Normalize(input);
|
|
}
|
|
|
|
float delta = GetFrameTime();
|
|
|
|
if (!body.IsGrounded)
|
|
{
|
|
body.Velocity.Y -= GRAVITY * delta;
|
|
}
|
|
|
|
if (body.IsGrounded && jumpPressed)
|
|
{
|
|
body.Velocity.Y = JUMP_FORCE;
|
|
body.IsGrounded = false;
|
|
|
|
// Sound can be played at this moment
|
|
//SetSoundPitch(fxJump, 1.0f + (GetRandomValue(-100, 100)*0.001));
|
|
//PlaySound(fxJump);
|
|
}
|
|
|
|
Vector3 front = new Vector3(MathF.Sin(rot), 0.0f, MathF.Cos(rot));
|
|
Vector3 right = new Vector3(MathF.Cos(-rot), 0.0f, MathF.Sin(-rot));
|
|
|
|
Vector3 desiredDir = new Vector3(
|
|
input.X * right.X + input.Y * front.X,
|
|
0.0f,
|
|
input.X * right.Z + input.Y * front.Z);
|
|
body.Dir = Vector3Lerp(body.Dir, desiredDir, CONTROL * delta);
|
|
|
|
float decel = (body.IsGrounded ? FRICTION : AIR_DRAG);
|
|
Vector3 hvel = new Vector3(body.Velocity.X * decel, 0.0f, body.Velocity.Z * decel);
|
|
|
|
float hvelLength = Vector3Length(hvel); // Magnitude
|
|
if (hvelLength < (MAX_SPEED * 0.01f))
|
|
{
|
|
hvel = new Vector3(0, 0, 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 maxSpeed = (crouchHold ? CROUCH_SPEED : MAX_SPEED);
|
|
float accel = Clamp(maxSpeed - speed, 0.0f, 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.0f)
|
|
{
|
|
body.Position.Y = 0.0f;
|
|
body.Velocity.Y = 0.0f;
|
|
body.IsGrounded = true; // Enable jumping
|
|
}
|
|
}
|
|
|
|
// Update camera for FPS behaviour
|
|
private void UpdateCameraFPS(ref Camera3D camera)
|
|
{
|
|
Vector3 up = new Vector3(0.0f, 1.0f, 0.0f);
|
|
Vector3 targetOffset = new Vector3(0.0f, 0.0f, -1.0f);
|
|
|
|
// Left and 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 and down
|
|
Vector3 right = Vector3Normalize(Vector3CrossProduct(yaw, up));
|
|
|
|
// Rotate view vector around right axis
|
|
float pitchAngle = -lookRotation.Y - lean.Y;
|
|
pitchAngle = Clamp(pitchAngle, -MathF.PI / 2 + 0.0001f, MathF.PI / 2 - 0.0001f); // Clamp angle so it doesn't go past straight up or straight down
|
|
Vector3 pitch = Vector3RotateByAxisAngle(yaw, right, pitchAngle);
|
|
|
|
// Head animation
|
|
// Rotate up direction around forward axis
|
|
float headSin = MathF.Sin(headTimer * MathF.PI);
|
|
float headCos = MathF.Cos(headTimer * MathF.PI);
|
|
const float stepRotation = 0.01f;
|
|
camera.Up = Vector3RotateByAxisAngle(up, pitch, headSin * stepRotation + lean.X);
|
|
|
|
// Camera BOB
|
|
const float bobSide = 0.1f;
|
|
const float bobUp = 0.15f;
|
|
Vector3 bobbing = Vector3Scale(right, headSin * bobSide);
|
|
bobbing.Y = MathF.Abs(headCos * bobUp);
|
|
|
|
camera.Position = Vector3Add(camera.Position, Vector3Scale(bobbing, walkLerp));
|
|
camera.Target = Vector3Add(camera.Position, pitch);
|
|
}
|
|
|
|
// Draw game level
|
|
private void DrawLevel()
|
|
{
|
|
const int floorExtent = 25;
|
|
const float tileSize = 5.0f;
|
|
Color tileColor1 = new Color(150, 200, 200, 255);
|
|
|
|
// Floor tiles
|
|
for (int y = -floorExtent; y < floorExtent; y++)
|
|
{
|
|
for (int x = -floorExtent; x < floorExtent; x++)
|
|
{
|
|
if ((y & 1) != 0 && (x & 1) != 0)
|
|
{
|
|
DrawPlane(new Vector3(x * tileSize, 0.0f, y * tileSize), new Vector2(tileSize, tileSize), tileColor1);
|
|
}
|
|
else if ((y & 1) == 0 && (x & 1) == 0)
|
|
{
|
|
DrawPlane(new Vector3(x * tileSize, 0.0f, y * tileSize), new Vector2(tileSize, tileSize), Color.LightGray);
|
|
}
|
|
}
|
|
}
|
|
|
|
Vector3 towerSize = new Vector3(16.0f, 32.0f, 16.0f);
|
|
Color towerColor = new Color(150, 200, 200, 255);
|
|
|
|
Vector3 towerPos = new Vector3(16.0f, 16.0f, 16.0f);
|
|
DrawCubeV(towerPos, towerSize, towerColor);
|
|
DrawCubeWiresV(towerPos, towerSize, Color.DarkBlue);
|
|
|
|
towerPos.X *= -1;
|
|
DrawCubeV(towerPos, towerSize, towerColor);
|
|
DrawCubeWiresV(towerPos, towerSize, Color.DarkBlue);
|
|
|
|
towerPos.Z *= -1;
|
|
DrawCubeV(towerPos, towerSize, towerColor);
|
|
DrawCubeWiresV(towerPos, towerSize, Color.DarkBlue);
|
|
|
|
towerPos.X *= -1;
|
|
DrawCubeV(towerPos, towerSize, towerColor);
|
|
DrawCubeWiresV(towerPos, towerSize, Color.DarkBlue);
|
|
|
|
// Red sun
|
|
DrawSphere(new Vector3(300.0f, 300.0f, 0.0f), 100.0f, new Color(255, 0, 0, 255));
|
|
}
|
|
|
|
public static int Main()
|
|
{
|
|
// Initialization
|
|
//--------------------------------------------------------------------------------------
|
|
InitWindow(screenWidth, screenHeight, "raylib [core] example - 3d camera fps");
|
|
|
|
DisableCursor(); // Limit cursor to relative movement inside the window
|
|
|
|
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
|
|
//--------------------------------------------------------------------------------------
|
|
|
|
var game = new Camera3dFps();
|
|
game.Init();
|
|
|
|
// Main game loop
|
|
while (!WindowShouldClose()) // Detect window close button or ESC key
|
|
{
|
|
game.Update();
|
|
}
|
|
|
|
game.Unload();
|
|
|
|
// De-Initialization
|
|
//--------------------------------------------------------------------------------------
|
|
CloseWindow(); // Close window and OpenGL context
|
|
//--------------------------------------------------------------------------------------
|
|
|
|
return 0;
|
|
}
|
|
}
|