using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osuTK; using osuTK.Graphics; using osuTK.Input; using Triangle = osu.Framework.Graphics.Shapes.Triangle; namespace HillLike; public class World : CompositeDrawable { private readonly Camera _camera = new(); private readonly CarDrawable _carDrawable; private readonly TerrainDrawable _terrainDrawable; private readonly Container _worldLayer; public readonly SimpleCar Car = new(); public World() { RelativeSizeAxes = Axes.Both; InternalChildren = [ new Box { RelativeSizeAxes = Axes.Both, Colour = new Color4(150, 210, 255, 255) }, _worldLayer = new Container { RelativeSizeAxes = Axes.Both, Children = [ _terrainDrawable = new TerrainDrawable(), _carDrawable = new CarDrawable(Car) ] } ]; Reset(); } public sealed override Axes RelativeSizeAxes { get => base.RelativeSizeAxes; set => base.RelativeSizeAxes = value; } public void Reset() { Car.Reset(new Vector2(0, Terrain.HeightAt(0) + 3f)); _camera.Reset(Car.Position); } public void Step(float dt) { dt = Math.Clamp(dt, 0, 1 / 20f); Car.Step(dt); _camera.Follow(dt, Car.Position); var pixelsPerUnit = _camera.PixelsPerUnit; var viewCenter = DrawSize / 2; _terrainDrawable.SetCamera(_camera, viewCenter); _carDrawable.SetCamera(_camera, viewCenter); } public void HandleKeyDown(KeyDownEvent e) { Car.HandleKeyDown(e); if (e.Key is Key.R) Reset(); } public void HandleKeyUp(KeyUpEvent e) { Car.HandleKeyUp(e); } private class Camera { public readonly float PixelsPerUnit = 60f; public Vector2 Position; public void Reset(Vector2 pos) { Position = pos; } public void Follow(float dt, Vector2 target) { var desired = target + new Vector2(5f, 2f); var smooth = 1f - MathF.Exp(-dt * 4.5f); Position = Vector2.Lerp(Position, desired, smooth); } } private abstract class WorldDrawable : CompositeDrawable { protected Camera? Camera; protected Vector2 ViewCenter; public void SetCamera(Camera cam, Vector2 viewCenter) { Camera = cam; ViewCenter = viewCenter; UpdateFromWorld(); } protected Vector2 ToScreen(Vector2 worldPos) { var rel = worldPos - Camera.Position; return new Vector2( ViewCenter.X + rel.X * Camera.PixelsPerUnit, ViewCenter.Y - rel.Y * Camera.PixelsPerUnit ); } protected float ToScreen(float worldLen) { return worldLen * Camera.PixelsPerUnit; } protected abstract void UpdateFromWorld(); } private class TerrainDrawable : WorldDrawable { private readonly Container _segments = new() { RelativeSizeAxes = Axes.Both }; public TerrainDrawable() { RelativeSizeAxes = Axes.Both; AddInternal(_segments); } protected override void UpdateFromWorld() { if (Camera is null) return; _segments.Clear(); var halfWidthWorld = DrawWidth / Camera.PixelsPerUnit; var left = Camera.Position.X - halfWidthWorld - 2f; var right = Camera.Position.X + halfWidthWorld + 2f; var step = 0.6f; var count = (int)Math.Ceiling((right - left) / step); for (var i = 0; i < count; i += 1) { var x0 = left + i * step; var x1 = x0 + step; var y0 = Terrain.HeightAt(x0); var y1 = Terrain.HeightAt(x1); var y = MathF.Min(y0, y1); var depth = 200f; var p = ToScreen(new Vector2(x0, y)); var w = ToScreen(step); var h = ToScreen(depth); //_segments.Add(new Box //{ // Anchor = Anchor.TopLeft, // Origin = Anchor.TopLeft, // Position = p, // Size = new Vector2(w + 1, h), // Colour = new Color4(80, 200, 120, 255) //}); _segments.Add(new Triangle { Anchor = Anchor.TopLeft, Origin = Anchor.BottomLeft, Position = p, Size = new Vector2(w + 1, -h), Shear = new Vector2(x1 - x0, y1 - y0), Colour = new Color4(80, 200, 120, 255) }); } for (var i = 0; i < count; i += 1) { var x0 = left + i * step; var x1 = x0 + step; var y0 = Terrain.HeightAt(x0); var y1 = Terrain.HeightAt(x1); var y = (y0 + y1) / 2f; var p = ToScreen(new Vector2(x0, y)); _segments.Add(new Box { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, Position = p, Size = new Vector2(ToScreen(step) + 1, 3), Colour = new Color4(20, 120, 60, 255) }); } } } private class CarDrawable : WorldDrawable { private readonly Box _body; private readonly SimpleCar _car; private readonly Circle _wheelBack; private readonly Circle _wheelFront; public CarDrawable(SimpleCar car) { _car = car; RelativeSizeAxes = Axes.Both; InternalChildren = new Drawable[] { _wheelBack = new Circle { Colour = new Color4(25, 25, 25, 255) }, _wheelFront = new Circle { Colour = new Color4(25, 25, 25, 255) }, _body = new Box { Colour = new Color4(240, 70, 70, 255) } }; } protected override void UpdateFromWorld() { if (Camera is null) return; var wheelR = SimpleCar.WheelRadius; var bodyW = 2.2f; var bodyH = 0.7f; var wb = ToScreen(_car.WheelBackPos); var wf = ToScreen(_car.WheelFrontPos); var rPx = ToScreen(wheelR); _wheelBack.Size = new Vector2(rPx * 2); _wheelBack.Position = wb - new Vector2(rPx); _wheelFront.Size = new Vector2(rPx * 2); _wheelFront.Position = wf - new Vector2(rPx); var bp = ToScreen(_car.Position); _body.Size = new Vector2(ToScreen(bodyW), ToScreen(bodyH)); _body.Origin = Anchor.Centre; _body.Anchor = Anchor.TopLeft; _body.Position = bp; _body.Rotation = -_car.RotationDegrees; } } } public class Terrain { public static float HeightAt(float x) { const float baseLine = 0f; var h = 0.9f * MathF.Sin(x * 0.55f) + 0.6f * MathF.Sin(x * 0.18f + 1.3f) + 0.25f * MathF.Sin(x * 1.15f); var drift = 0.015f * x; return baseLine + h + drift; } public static float SlopeAt(float x) { const float eps = 0.02f; return (HeightAt(x + eps) - HeightAt(x - eps)) / (2 * eps); } public static Vector2 NormalAt(float x) { var slope = SlopeAt(x); var n = new Vector2(-slope, 1f); return n.Normalized(); } }