1
0
hook-climber/Program.cs
2024-06-09 14:39:42 +03:00

459 lines
14 KiB
C#

using static Raylib_cs.Raylib;
using Raylib_cs;
using static Raylib_cs.Raymath;
using System.Numerics;
namespace Hook_Climber;
static class Utils
{
public static Rectangle centeredRect(Vector2 center, Vector2 size)
{
return new Rectangle(center - size / 2, size);
}
}
class Game {
public static float playerMaxSpeed = 650.0f;
public static float playerWalkForce = 4500.0f;
public static float playerFriction = 0.999f;
public static float playerGravity = 2000.0f;
public static float hookDistance = 75f;
public static float hookPower = 300f;
public static float hookGravity = 600.0f;
public static float hookOffset = 50f;
public static float hookRopeStrength = 30f;
public static Vector2 hookSize = new Vector2(10, 10);
public static Vector2 playerSize = new Vector2(50, 50);
public Player player;
public List<Rectangle> platforms;
public static Rectangle GetSweptBroadphaseBox(Rectangle b, Vector2 v)
{
Rectangle broadphasebox = b;
broadphasebox.X = v.X > 0 ? b.X : b.X + v.X;
broadphasebox.Y = v.Y > 0 ? b.Y : b.Y + v.Y;
broadphasebox.Width = v.X > 0 ? v.X + b.Width : b.Width - v.X;
broadphasebox.Height = v.Y > 0 ? v.Y + b.Height : b.Height - v.Y;
return broadphasebox;
}
public IEnumerable<(Rectangle, float, Vector2)> IterCollisions(Rectangle collider, Vector2 velocity, float dt)
{
foreach (var platform in platforms)
{
Vector2 normal;
float collisiontime = CheckCollision(
collider,
velocity * dt,
platform,
out normal
);
if (normal.X == 0 && normal.Y == 0) continue;
yield return (platform, collisiontime * dt, normal);
}
}
public static float CheckCollision(Rectangle b1, Vector2 v1, Rectangle b2, out Vector2 normal)
{
normal.X = 0;
normal.Y = 0;
if (!CheckCollisionRecs(GetSweptBroadphaseBox(b1, v1), b2))
{
return 1;
}
float entryX, exitX;
if (v1.X > 0)
{
entryX = b2.X - (b1.X + b1.Width);
exitX = (b2.X + b2.Width) - b1.X;
}
else
{
entryX = (b2.X + b2.Width) - b1.X;
exitX = b2.X - (b1.X + b1.Width);
}
float entryY, exitY;
if (v1.Y > 0)
{
entryY = b2.Y - (b1.Y + b1.Height);
exitY = (b2.Y + b2.Height) - b1.Y;
}
else
{
entryY = (b2.Y + b2.Height) - b1.Y;
exitY = b2.Y - (b1.Y + b1.Height);
}
float entryXTime, exitXTime;
if (v1.X == 0)
{
entryXTime = float.MinValue;
exitXTime = float.MaxValue;
}
else
{
entryXTime = entryX / v1.X;
exitXTime = exitX / v1.X;
}
float entryYTime, exitYTime;
if (v1.Y == 0)
{
entryYTime = float.MinValue;
exitYTime = float.MaxValue;
}
else
{
entryYTime = entryY / v1.Y;
exitYTime = exitY / v1.Y;
}
float entryTime = Math.Max(entryXTime, entryYTime);
float exitTime = Math.Min(exitXTime, exitYTime);
if (entryTime > exitTime || entryXTime < 0 && entryYTime < 0 || entryXTime > 1.0f || entryYTime > 1.0f)
{
return 1;
}
if (entryXTime > entryYTime)
{
normal.X = entryX < 0.0 ? 1 : -1;
}
else
{
normal.Y = entryY < 0.0 ? 1 : -1;
}
return entryTime;
}
}
class Hook
{
public Game game;
public Vector2 offset;
public Vector2 dir;
public Vector2 primedAt;
public bool shown;
public bool primed;
public bool headReleased;
public Vector2 headPosition;
public Vector2 headVelocity;
public bool headGrounded;
public float reelDir = 0;
public float idealRopeLength = 200f;
public GamepadAxis moveX;
public GamepadAxis moveY;
public GamepadButton use;
public GamepadButton prime;
public void resetHead()
{
headGrounded = false;
headReleased = false;
}
public List<Vector2> headProjection(uint steps, float dt)
{
var points = new List<Vector2>();
var position = headPosition;
var velocity = headVelocity;
for (int i = 0; i < steps; i++)
{
if (updatePhysics(ref position, ref velocity, dt)) break;
points.Add(position);
}
return points;
}
public bool updatePhysics(ref Vector2 position, ref Vector2 velocity, float dt)
{
velocity.Y += Game.hookGravity * dt;
bool collisionOccured = checkAndResolveCollisions(Utils.centeredRect(position, Game.hookSize), ref velocity, dt);
position += velocity * dt;
return collisionOccured;
}
public bool checkAndResolveCollisions(Rectangle collider, ref Vector2 velocity, float dt)
{
bool collision = false;
foreach (var (_, collisiontime, normal) in game.IterCollisions(collider, velocity, dt))
{
velocity.X *= 0;
velocity.Y *= 0;
//velocity.X = velocity.X * collisiontime / dt;
//velocity.Y = velocity.Y * collisiontime / dt;
collision = true;
}
return collision;
}
};
class Player
{
public Vector2 position = new Vector2(0, 0);
public Vector2 velocity = new Vector2(0, 0);
public Rectangle collisionRect()
{
return new Rectangle(position - Game.playerSize / 2, Game.playerSize.X, Game.playerSize.Y);
}
};
internal class Program
{
public static void Main()
{
InitWindow(2000, 1200, "Hook Climber");
SetWindowState(ConfigFlags.ResizableWindow);
var game = new Game();
game.player = new Player();
var leftHook = new Hook
{
game = game,
offset = new Vector2(-Game.hookOffset, 0),
moveX = GamepadAxis.LeftX,
moveY = GamepadAxis.LeftY,
use = GamepadButton.LeftTrigger2,
prime = GamepadButton.LeftTrigger1,
idealRopeLength = 200
};
var rightHook = new Hook
{
game = game,
offset = new Vector2(Game.hookOffset, 0),
moveX = GamepadAxis.RightX,
moveY = GamepadAxis.RightY,
use = GamepadButton.RightTrigger2,
prime = GamepadButton.RightTrigger1,
idealRopeLength = 200
};
var hooks = new Hook[]{ leftHook, rightHook };
var gamepadId = 0;
game.platforms = new List<Rectangle> {
new Rectangle(-450, game.player.position.Y + Game.playerSize.Y/2, 1000, 20),
new Rectangle(0, -200, 100, 10),
new Rectangle(-100, -400, 100, 10),
new Rectangle(100, -600, 100, 10),
new Rectangle(-200, -800, 100, 10),
new Rectangle(-300, -1000, 100, 10),
};
var player = game.player;
var camera = new Camera2D();
camera.Target = player.position;
while (!WindowShouldClose())
{
var dt = GetFrameTime();
var windowWidth = GetScreenWidth();
var windowHeight = GetScreenHeight();
camera.Offset = new Vector2(windowWidth / 2, windowHeight / 2);
camera.Target = Vector2.Lerp(camera.Target, player.position, 20 * dt);
camera.Zoom = 1;
BeginDrawing();
ClearBackground(Color.White);
BeginMode2D(camera);
var dx = 0f;
if (IsKeyDown(KeyboardKey.D))
{
dx += 1;
}
if (IsKeyDown(KeyboardKey.A))
{
dx -= 1;
}
if (IsGamepadAvailable(gamepadId))
{
foreach (var hook in hooks)
{
if (IsGamepadButtonPressed(gamepadId, hook.prime))
{
hook.primedAt = hook.dir;
}
if (IsGamepadButtonDown(gamepadId, hook.prime))
{
var trajectory = hook.primedAt - hook.dir;
hook.headPosition = player.position + hook.primedAt * Game.hookDistance + hook.offset;
hook.headVelocity = trajectory * Game.hookPower + player.velocity;
}
if (IsGamepadButtonReleased(gamepadId, hook.prime))
{
if (hook.headReleased && (hook.primedAt - hook.dir).Length() < 0.01)
{
hook.resetHead();
}
else if (hook.primed && hook.shown)
{
hook.headReleased = true;
}
}
hook.shown = IsGamepadButtonDown(gamepadId, hook.use);
hook.primed = IsGamepadButtonDown(gamepadId, hook.prime);
if (!hook.headReleased)
{
hook.dir.X = GetGamepadAxisMovement(gamepadId, hook.moveX);
hook.dir.Y = GetGamepadAxisMovement(gamepadId, hook.moveY);
} else
{
hook.dir = new Vector2();
if (IsGamepadButtonDown(gamepadId, hook.use))
{
hook.reelDir = GetGamepadAxisMovement(gamepadId, hook.moveY);
} else
{
hook.reelDir = 0;
}
}
}
dx += GetGamepadAxisMovement(gamepadId, GamepadAxis.LeftX);
}
dx = Clamp(dx, -1, 1);
if (leftHook.shown)
{
dx = 0;
}
player.velocity.X += dx * Game.playerWalkForce * dt;
player.velocity.Y += Game.playerGravity * dt;
player.velocity = Vector2ClampValue(player.velocity, 0, Game.playerMaxSpeed);
player.velocity *= (float)Math.Pow(1 - Game.playerFriction, dt);
foreach (var (_, collisiontime, normal) in game.IterCollisions(player.collisionRect(), player.velocity, dt))
{
float remainingtime = (1.0f - collisiontime);
float dotprod = (player.velocity.X * normal.Y + player.velocity.Y * normal.X) * remainingtime;
// player.velocity.X = dotprod * normal.Y;
player.velocity.Y = dotprod * normal.X;
}
player.position += player.velocity * dt;
DrawCircle(0, 0, 5, Color.Red);
DrawRectangleRec(player.collisionRect(), Color.Black);
foreach (var hook in hooks)
{
var handPosition = player.position + hook.dir * Game.hookDistance + hook.offset;
if (hook.headReleased)
{
if (!hook.headGrounded)
{
hook.headGrounded = hook.updatePhysics(ref hook.headPosition, ref hook.headVelocity, dt);
if (hook.headGrounded)
{
hook.idealRopeLength = (handPosition - hook.headPosition).Length();
}
}
if (hook.headGrounded)
{
hook.idealRopeLength += hook.reelDir * dt * 150;
hook.idealRopeLength = Math.Max(hook.idealRopeLength, 0f);
var handToHead = hook.headPosition - handPosition;
player.velocity += (handToHead.Length() - hook.idealRopeLength) * Vector2Normalize(handToHead) * Game.hookRopeStrength * dt;
}
DrawRectangleRec(Utils.centeredRect(hook.headPosition, Game.hookSize), Color.DarkBlue);
DrawLineV(handPosition, hook.headPosition, Color.Blue);
}
if (!hook.shown && !hook.headReleased) continue;
if (hook.primed)
{
var primePosition = player.position + hook.primedAt * Game.hookDistance + hook.offset;
DrawRectangleRec(Utils.centeredRect(primePosition, Game.hookSize), Color.DarkGray);
DrawLineV(handPosition, primePosition, Color.Gray);
var projection = hook.headProjection(100, 1f / 60f);
if (projection.Count > 0)
{
var projectionArray = projection.ToArray();
unsafe
{
fixed (Vector2* pointerToFirst = &projectionArray[0])
{
DrawLineStrip(pointerToFirst, projectionArray.Length, Color.Blue);
}
}
}
}
DrawRectangleRec(Utils.centeredRect(handPosition, Game.hookSize), Color.Blue);
}
foreach (var platform in game.platforms)
{
DrawRectangleRec(platform, Color.Gray);
}
EndMode2D();
{ // Speed bar
var speedWidth = 300.0f;
DrawText("Speed", 10, 10, 20, Color.Black);
DrawRectangleRec(new Rectangle(75, 10, speedWidth, 20), Color.Gray);
DrawRectangleRec(new Rectangle(75, 10, speedWidth * (player.velocity.Length() / Game.playerMaxSpeed), 20), Color.Red);
DrawText($"{player.velocity.X:N3}", 400, 10, 20, Color.Black);
DrawText($"{player.velocity.Y:N3}", 500, 10, 20, Color.Black);
}
{ // Rope lengths
DrawText($"Left ideal rope length: {leftHook.idealRopeLength}", 10, 30, 20, Color.Black);
DrawText($"Right ideal rope length: {rightHook.idealRopeLength}", 10, 50, 20, Color.Black);
}
EndDrawing();
}
CloseWindow();
}
}