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 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 headProjection(uint steps, float dt) { var points = new List(); 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 { 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(); } }