// Asteroids — arrows turn and thrust, Space fires. Vector paths through a raw Skia hook.
const double W = 380, H = 480;
const double turnRate = 3.4; // rad/s
const double thrustAcc = 230; // dip/s^2
const double drag = 0.38;
const double maxSpeed = 270;
const double shotSpeed = 330, shotLife = 0.85, fireCool = 0.22;
const double bigR = 34;
var rnd = new Random();
var rocks = new List<double[]>(); // x, y, vx, vy, r, angle, spin
var edges = new List<float[]>(); // matching polygon for each rock
var shots = new List<double[]>(); // x, y, vx, vy, life
double shipX = W / 2, shipY = H / 2, shipVX = 0, shipVY = 0, aim = -Math.PI / 2;
double cool = 0, invuln = 0, blink = 0;
int score = 0, lives = 3, wave = 0, rot = 0;
bool ptr = false;
double ptrX = 0, ptrY = 0;
bool over = false, thrusting = false;
SkiaLabel hud = null;
float[] RockEdges(double r)
{
var n = 9;
var pts = new float[n * 2];
for (int i = 0; i < n; i++)
{
var a = i * Math.PI * 2 / n;
var rad = r * (0.7 + rnd.NextDouble() * 0.45);
pts[i * 2] = (float)(Math.Cos(a) * rad);
pts[i * 2 + 1] = (float)(Math.Sin(a) * rad);
}
return pts;
}
void AddRock(double x, double y, double r)
{
var a = rnd.NextDouble() * Math.PI * 2;
var sp = 20 + rnd.NextDouble() * 45 + wave * 4;
rocks.Add(new double[] { x, y, Math.Cos(a) * sp, Math.Sin(a) * sp, r, rnd.NextDouble() * 6.28, (rnd.NextDouble() - 0.5) * 1.8 });
edges.Add(RockEdges(r));
}
void SpawnWave()
{
wave++;
for (int i = 0; i < 3 + wave; i++)
{
double x, y;
do
{
x = rnd.NextDouble() * W;
y = rnd.NextDouble() * H;
}
while (Math.Abs(x - shipX) < 100 && Math.Abs(y - shipY) < 100);
AddRock(x, y, bigR);
}
}
void Respawn()
{
shipX = W / 2; shipY = H / 2; shipVX = shipVY = 0; aim = -Math.PI / 2;
invuln = 2;
}
void Reset()
{
rocks.Clear(); edges.Clear(); shots.Clear();
score = 0; lives = 3; wave = 0; cool = 0; rot = 0;
over = false; thrusting = false;
Respawn();
SpawnWave();
}
Reset();
void Fire()
{
if (over || cool > 0) return;
cool = fireCool;
shots.Add(new double[]
{
shipX + Math.Cos(aim) * 12,
shipY + Math.Sin(aim) * 12,
shipVX + Math.Cos(aim) * shotSpeed,
shipVY + Math.Sin(aim) * shotSpeed,
shotLife,
});
}
void Wrap(double[] o)
{
if (o[0] < 0) o[0] += W; else if (o[0] > W) o[0] -= W;
if (o[1] < 0) o[1] += H; else if (o[1] > H) o[1] -= H;
}
void Kill(int i)
{
var r = rocks[i];
score += r[4] > 26 ? 20 : r[4] > 16 ? 50 : 100;
if (r[4] > 18)
{
AddRock(r[0], r[1], r[4] * 0.55);
AddRock(r[0], r[1], r[4] * 0.55);
}
rocks.RemoveAt(i);
edges.RemoveAt(i);
}
void Step(double dt)
{
blink += dt;
if (over) return;
cool -= dt;
invuln -= dt;
aim += rot * turnRate * dt;
// mouse / touch: hold anywhere, the ship turns toward the pointer, thrusts and fires
if (ptr)
{
var want = Math.Atan2(ptrY - shipY, ptrX - shipX);
var d = want - aim;
while (d > Math.PI) d -= Math.PI * 2;
while (d < -Math.PI) d += Math.PI * 2;
var maxTurn = turnRate * dt;
aim += Math.Clamp(d, -maxTurn, maxTurn);
var dist = Math.Sqrt((ptrX - shipX) * (ptrX - shipX) + (ptrY - shipY) * (ptrY - shipY));
thrusting = dist > 40;
if (Math.Abs(d) < 0.5) Fire();
}
if (thrusting)
{
shipVX += Math.Cos(aim) * thrustAcc * dt;
shipVY += Math.Sin(aim) * thrustAcc * dt;
var sp = Math.Sqrt(shipVX * shipVX + shipVY * shipVY);
if (sp > maxSpeed) { shipVX = shipVX / sp * maxSpeed; shipVY = shipVY / sp * maxSpeed; }
}
shipVX -= shipVX * drag * dt;
shipVY -= shipVY * drag * dt;
shipX += shipVX * dt;
shipY += shipVY * dt;
if (shipX < 0) shipX += W; else if (shipX > W) shipX -= W;
if (shipY < 0) shipY += H; else if (shipY > H) shipY -= H;
for (int i = 0; i < rocks.Count; i++)
{
var r = rocks[i];
r[0] += r[2] * dt;
r[1] += r[3] * dt;
r[5] += r[6] * dt;
Wrap(r);
}
for (int i = shots.Count - 1; i >= 0; i--)
{
var b = shots[i];
b[0] += b[2] * dt;
b[1] += b[3] * dt;
b[4] -= dt;
Wrap(b);
if (b[4] <= 0) { shots.RemoveAt(i); continue; }
for (int k = rocks.Count - 1; k >= 0; k--)
{
var r = rocks[k];
var dx = b[0] - r[0];
var dy = b[1] - r[1];
if (dx * dx + dy * dy < r[4] * r[4])
{
Kill(k);
shots.RemoveAt(i);
break;
}
}
}
if (invuln <= 0)
{
for (int k = rocks.Count - 1; k >= 0; k--)
{
var r = rocks[k];
var dx = shipX - r[0];
var dy = shipY - r[1];
var hit = r[4] + 8;
if (dx * dx + dy * dy < hit * hit)
{
Kill(k);
if (--lives <= 0) over = true; else Respawn();
break;
}
}
}
if (rocks.Count == 0) SpawnWave();
if (hud != null)
hud.Text = over
? $"GAME OVER {score} — SPACE"
: $"{score} {new string('^', Math.Max(0, lives))} wave {wave} — hold the mouse to fly";
}
return new SkiaShape
{
CornerRadius = 10,
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center,
BackgroundColor = Color.Parse("#05070D"),
WidthRequest = W,
HeightRequest = H,
Children = new List<SkiaControl>
{
new SkiaLayer()
.Fill()
.WhenPaint((me, ctx) =>
{
var canvas = ctx.Context.Canvas;
var s = (float)ctx.Scale;
var ox = ctx.Destination.Left;
var oy = ctx.Destination.Top;
canvas.Save();
canvas.ClipRect(ctx.Destination);
using var line = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Stroke,
StrokeWidth = 1.6f * s,
StrokeCap = SKStrokeCap.Round,
Color = SKColors.White,
};
using var dot = new SKPaint { IsAntialias = true, Color = SKColors.White };
for (int i = 0; i < rocks.Count; i++)
{
var r = rocks[i];
var p = edges[i];
using var path = new SKPath();
for (int k = 0; k < p.Length; k += 2)
{
if (k == 0) path.MoveTo(p[0] * s, p[1] * s);
else path.LineTo(p[k] * s, p[k + 1] * s);
}
path.Close();
line.Color = r[4] > 26 ? SKColors.White : r[4] > 16 ? SKColor.Parse("#B9D4FF") : SKColor.Parse("#7FA8E8");
// draw the wrapped copies too, so a rock crossing an edge is whole on both sides
for (int wx = -1; wx <= 1; wx++)
for (int wy = -1; wy <= 1; wy++)
{
var px = r[0] + wx * W;
var py = r[1] + wy * H;
if (px < -r[4] || px > W + r[4] || py < -r[4] || py > H + r[4]) continue;
canvas.Save();
canvas.Translate((float)(ox + px * s), (float)(oy + py * s));
canvas.RotateDegrees((float)(r[5] * 180 / Math.PI));
canvas.DrawPath(path, line);
canvas.Restore();
}
}
foreach (var b in shots)
canvas.DrawCircle((float)(ox + b[0] * s), (float)(oy + b[1] * s), 1.8f * s, dot);
var visible = !over && (invuln <= 0 || (int)(blink * 12) % 2 == 0);
if (visible)
{
using var ship = new SKPath();
ship.MoveTo(14 * s, 0);
ship.LineTo(-9 * s, -8 * s);
ship.LineTo(-5 * s, 0);
ship.LineTo(-9 * s, 8 * s);
ship.Close();
using var flame = new SKPath();
flame.MoveTo(-6 * s, -4 * s);
flame.LineTo(-15 * s, 0);
flame.LineTo(-6 * s, 4 * s);
for (int wx = -1; wx <= 1; wx++)
for (int wy = -1; wy <= 1; wy++)
{
var px = shipX + wx * W;
var py = shipY + wy * H;
if (px < -16 || px > W + 16 || py < -16 || py > H + 16) continue;
canvas.Save();
canvas.Translate((float)(ox + px * s), (float)(oy + py * s));
canvas.RotateDegrees((float)(aim * 180 / Math.PI));
line.Color = SKColor.Parse("#63D7FF");
canvas.DrawPath(ship, line);
if (thrusting)
{
line.Color = SKColor.Parse("#FF8A3D");
canvas.DrawPath(flame, line);
}
canvas.Restore();
}
}
canvas.Restore();
})
.WithGestures((me, args, apply) =>
{
var px = (args.Event.Location.X - me.DrawingRect.Left) / me.RenderingScale;
var py = (args.Event.Location.Y - me.DrawingRect.Top) / me.RenderingScale;
if (args.Type == TouchActionResult.Down)
{
if (over) { Reset(); return me; }
ptr = true; ptrX = px; ptrY = py;
Fire();
return me;
}
if (args.Type == TouchActionResult.Panning)
{
ptrX = px; ptrY = py;
return me;
}
if (args.Type == TouchActionResult.Up)
{
ptr = false;
thrusting = false;
return me;
}
return null;
})
.Animate(1, (me, animator, value, dt) =>
{
Step(Math.Min(dt, 0.05));
me.Update();
}, repeat: -1),
new SkiaLabel("0")
{
FontSize = 13,
TextColor = Color.Parse("#8AA0C0"),
Margin = new Thickness(0, 10, 0, 0),
}.CenterX().Assign(out hud),
}
}
.OnKeyDown((me, key) =>
{
if (over)
{
if (key == InputKey.Space || key == InputKey.Enter) Reset();
return;
}
if (key == InputKey.ArrowLeft) rot = -1;
else if (key == InputKey.ArrowRight) rot = 1;
else if (key == InputKey.ArrowUp) thrusting = true;
else if (key == InputKey.Space || key == InputKey.Enter) Fire();
})
.OnKeyUp((me, key) =>
{
if (key == InputKey.ArrowLeft && rot < 0) rot = 0;
else if (key == InputKey.ArrowRight && rot > 0) rot = 0;
else if (key == InputKey.ArrowUp) thrusting = false;
});