// Space Invaders — tiny proto. Arrows or drag to move, Space or tap to fire.
const int cols = 9, rows = 4;
const double fieldW = 360, fieldH = 500;
const double invW = 20, invH = 13, gapX = 10, gapY = 12;
const double marchX = 10, marchTop = 44; // swarm origin inside the field
const double dropStep = 10; // dip gained every time the swarm turns
const double baseMarch = 26; // dip/s with a full swarm, faster as they die
const double shipW = 28, shipH = 10, shipY = 468;
const double shipSpeed = 300;
const double shotSpeed = 430, bombSpeed = 180;
const double bombRate = 0.9; // bombs per second for the whole swarm
const double fireDelay = 0.3;
const int maxShots = 3, maxBombs = 7;
var rnd = new Random();
var alive = new bool[cols, rows];
var shots = new List<double[]>(); // x, y
var bombs = new List<double[]>();
double swarmX = 0, swarmY = 0, shipX = fieldW / 2, shipTo = fieldW / 2, cool = 0;
int dir = 1, left = 0, keyDir = 0, score = 0, lives = 3;
bool over = false, won = false;
SkiaLabel hud = null;
double InvX(int c) => marchX + swarmX + c * (invW + gapX);
double InvY(int r) => marchTop + swarmY + r * (invH + gapY);
void Reset()
{
for (int c = 0; c < cols; c++)
for (int r = 0; r < rows; r++)
alive[c, r] = true;
left = cols * rows;
swarmX = 0; swarmY = 0; dir = 1;
shots.Clear(); bombs.Clear();
shipX = shipTo = fieldW / 2;
score = 0; lives = 3; cool = 0;
over = false; won = false;
}
Reset();
void Fire()
{
if (over || won || cool > 0 || shots.Count >= maxShots) return;
cool = fireDelay;
shots.Add(new double[] { shipX, shipY });
}
void Step(double dt)
{
if (over || won) return;
cool -= dt;
// ship: keyboard holds a direction, the pointer chases a target
if (keyDir != 0) shipTo = shipX + keyDir * shipSpeed * dt;
shipTo = Math.Clamp(shipTo, shipW / 2, fieldW - shipW / 2);
shipX += (shipTo - shipX) * Math.Min(1.0, dt * 22);
// swarm marches, turns and drops at the walls, speeds up as it thins out
var speed = baseMarch * (1 + 2.0 * (1 - (double)left / (cols * rows)));
swarmX += dir * speed * dt;
var span = cols * (invW + gapX) - gapX;
var maxX = fieldW - marchX * 2 - span;
if (swarmX < 0 || swarmX > maxX)
{
swarmX = Math.Clamp(swarmX, 0, maxX);
dir = -dir;
swarmY += dropStep;
}
// bombs fall from the lowest invader of a random column
if (bombs.Count < maxBombs && rnd.NextDouble() < bombRate * dt)
{
var c = rnd.Next(cols);
for (int r = rows - 1; r >= 0; r--)
if (alive[c, r]) { bombs.Add(new double[] { InvX(c) + invW / 2, InvY(r) + invH }); break; }
}
for (int i = shots.Count - 1; i >= 0; i--)
{
var s = shots[i];
s[1] -= shotSpeed * dt;
if (s[1] < 0) { shots.RemoveAt(i); continue; }
var gone = false;
for (int c = 0; c < cols && !gone; c++)
for (int r = 0; r < rows; r++)
{
if (!alive[c, r]) continue;
var x = InvX(c); var y = InvY(r);
if (s[0] >= x && s[0] <= x + invW && s[1] >= y && s[1] <= y + invH)
{
alive[c, r] = false;
left--; score += 10;
gone = true;
break;
}
}
if (gone) shots.RemoveAt(i);
}
if (left == 0) won = true;
for (int i = bombs.Count - 1; i >= 0; i--)
{
var b = bombs[i];
b[1] += bombSpeed * dt;
if (b[1] > shipY + shipH) { bombs.RemoveAt(i); continue; }
if (b[1] >= shipY && Math.Abs(b[0] - shipX) < shipW / 2)
{
if (--lives <= 0) over = true;
bombs.Clear();
break;
}
}
// swarm landed on the ship line
for (int c = 0; c < cols && !over; c++)
for (int r = rows - 1; r >= 0; r--)
if (alive[c, r]) { if (InvY(r) + invH >= shipY) over = true; break; }
if (hud != null)
hud.Text = over ? $"GAME OVER {score} — SPACE"
: won ? $"CLEARED {score} — SPACE"
: $"{score} {new string('|', lives)}";
}
return new SkiaShape
{
CornerRadius = 10,
BackgroundColor = Color.Parse("#080B12"),
WidthRequest = fieldW,
HeightRequest = fieldH,
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;
using var paint = new SKPaint { IsAntialias = false };
void Box(double x, double y, double w, double h, string col)
{
paint.Color = SKColor.Parse(col);
canvas.DrawRect(new SKRect(
(float)(ox + x * s), (float)(oy + y * s),
(float)(ox + (x + w) * s), (float)(oy + (y + h) * s)), paint);
}
for (int c = 0; c < cols; c++)
for (int r = 0; r < rows; r++)
{
if (!alive[c, r]) continue;
var x = InvX(c); var y = InvY(r);
var col = r == 0 ? "#FF4D6D" : r == 1 ? "#FFC65C" : "#5CE1A0";
Box(x + 3, y, invW - 6, invH, col); // body
Box(x, y + 3, invW, invH - 7, col); // arms
Box(x + 4, y + 4, 3, 3, "#080B12"); // eyes
Box(x + invW - 7, y + 4, 3, 3, "#080B12");
Box(x, y + invH, 4, 3, col); // legs
Box(x + invW - 4, y + invH, 4, 3, col);
}
foreach (var b in bombs) Box(b[0] - 1.5, b[1], 3, 8, "#FF8A3D");
foreach (var sh in shots) Box(sh[0] - 1.5, sh[1], 3, 10, "#FFFFFF");
if (!over)
{
Box(shipX - shipW / 2, shipY, shipW, shipH, "#63D7FF");
Box(shipX - 2, shipY - 5, 4, 5, "#63D7FF");
}
})
.WithGestures((me, args, apply) =>
{
if (args.Type == TouchActionResult.Down || args.Type == TouchActionResult.Panning)
{
shipTo = (args.Event.Location.X - me.DrawingRect.Left) / me.RenderingScale;
if (args.Type == TouchActionResult.Down)
{
if (over || won) Reset(); else Fire();
}
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),
}
}
.CenterX()
.OnKeyDown((me, key) =>
{
if (over || won)
{
if (key == InputKey.Space || key == InputKey.Enter) Reset();
return;
}
if (key == InputKey.Space || key == InputKey.Enter || key == InputKey.ArrowUp) Fire();
if (key == InputKey.ArrowLeft) keyDir = -1;
else if (key == InputKey.ArrowRight) keyDir = 1;
})
.OnKeyUp((me, key) =>
{
if (key == InputKey.ArrowLeft && keyDir < 0) keyDir = 0;
else if (key == InputKey.ArrowRight && keyDir > 0) keyDir = 0;
});