feat: initial commit

Signed-off-by: Harald Hoyer <harald@hoyer.xyz>
This commit is contained in:
Harald Hoyer 2025-03-29 07:40:17 +01:00
commit 0a5382187e
10 changed files with 4877 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake .

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
/.direnv
/logs

3
.goosehints Normal file
View file

@ -0,0 +1,3 @@
You are on a nix system. This project contains a `flake.nix`.
To run any command use the nix develop shell.
Read the projects `README.md` for further instructions.

1
CLAUDE.md Symbolic link
View file

@ -0,0 +1 @@
.goosehints

4096
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

8
Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "bglga"
version = "0.1.0"
edition = "2021"
[dependencies]
bevy = "0.13"
fastrand = "2.0.1"

30
README.md Normal file
View file

@ -0,0 +1,30 @@
# bglga
This project is a simple Galaga-like space shooter game built using the Bevy engine.
## Current State
The game features:
- A player ship at the bottom of the screen that can move left and right
- Shooting bullets upward using the spacebar or up arrow key
- Enemy ships that spawn at the top and move downward
- Collision detection between bullets and enemies
## Controls
- Move Left: A key or Left Arrow
- Move Right: D key or Right Arrow
- Shoot: Spacebar or Up Arrow
## How to Compile
```
nix develop --command bash -c "cargo build"
```
## How to Run
1. Make sure you have Rust and Nix installed.
2. Clone the repository.
3. Navigate to the project directory.
4. Run the game using the command: `nix develop --command bash -c "cargo run"`

496
flake.lock Normal file
View file

@ -0,0 +1,496 @@
{
"nodes": {
"advisory-db": {
"flake": false,
"locked": {
"lastModified": 1742713221,
"narHash": "sha256-PbtSoLB1FEAMy++3kPW6Kh+o4pPy5Ks92jeUyYIdh5E=",
"owner": "rustsec",
"repo": "advisory-db",
"rev": "c8a7050cd4ecc35651f5a138a162bcb7730c5a9e",
"type": "github"
},
"original": {
"owner": "rustsec",
"repo": "advisory-db",
"type": "github"
}
},
"crane": {
"locked": {
"lastModified": 1742394900,
"narHash": "sha256-vVOAp9ahvnU+fQoKd4SEXB2JG2wbENkpqcwlkIXgUC0=",
"owner": "ipetkov",
"repo": "crane",
"rev": "70947c1908108c0c551ddfd73d4f750ff2ea67cd",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"crane_2": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": [
"nixify",
"nix-log",
"nixify",
"flake-utils"
],
"nixpkgs": [
"nixify",
"nix-log",
"nixify",
"nixpkgs"
],
"rust-overlay": [
"nixify",
"nix-log",
"nixify",
"rust-overlay"
]
},
"locked": {
"lastModified": 1679255352,
"narHash": "sha256-nkGwGuNkhNrnN33S4HIDV5NzkzMLU5mNStRn9sZwq8c=",
"owner": "rvolosatovs",
"repo": "crane",
"rev": "cec65880599a4ec6426186e24342e663464f5933",
"type": "github"
},
"original": {
"owner": "rvolosatovs",
"ref": "feat/wit",
"repo": "crane",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"nixify",
"nixpkgs-nixos"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1742452566,
"narHash": "sha256-sVuLDQ2UIWfXUBbctzrZrXM2X05YjX08K7XHMztt36E=",
"owner": "nix-community",
"repo": "fenix",
"rev": "7d9ba794daf5e8cc7ee728859bc688d8e26d5f06",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"fenix_2": {
"inputs": {
"nixpkgs": [
"nixify",
"nix-log",
"nixify",
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src_2"
},
"locked": {
"lastModified": 1679552560,
"narHash": "sha256-L9Se/F1iLQBZFGrnQJO8c9wE5z0Mf8OiycPGP9Y96hA=",
"owner": "nix-community",
"repo": "fenix",
"rev": "fb49a9f5605ec512da947a21cc7e4551a3950397",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1678901627,
"narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"macos-sdk": {
"flake": false,
"locked": {
"lastModified": 1694769349,
"narHash": "sha256-TEvVJy+NMPyzgWSk/6S29ZMQR+ICFxSdS3tw247uhFc=",
"type": "tarball",
"url": "https://github.com/roblabla/MacOSX-SDKs/releases/download/macosx14.0/MacOSX14.0.sdk.tar.xz"
},
"original": {
"type": "tarball",
"url": "https://github.com/roblabla/MacOSX-SDKs/releases/download/macosx14.0/MacOSX14.0.sdk.tar.xz"
}
},
"nix-filter": {
"locked": {
"lastModified": 1731533336,
"narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "f7653272fd234696ae94229839a99b73c9ab7de0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-filter",
"type": "github"
}
},
"nix-filter_2": {
"locked": {
"lastModified": 1678109515,
"narHash": "sha256-C2X+qC80K2C1TOYZT8nabgo05Dw2HST/pSn6s+n6BO8=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "aa9ff6ce4a7f19af6415fb3721eaa513ea6c763c",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-filter",
"type": "github"
}
},
"nix-flake-tests": {
"locked": {
"lastModified": 1677844186,
"narHash": "sha256-ErJZ/Gs1rxh561CJeWP5bohA2IcTq1rDneu1WT6CVII=",
"owner": "antifuchs",
"repo": "nix-flake-tests",
"rev": "bbd9216bd0f6495bb961a8eb8392b7ef55c67afb",
"type": "github"
},
"original": {
"owner": "antifuchs",
"repo": "nix-flake-tests",
"type": "github"
}
},
"nix-log": {
"inputs": {
"nix-flake-tests": "nix-flake-tests",
"nixify": "nixify_2",
"nixlib": "nixlib_2"
},
"locked": {
"lastModified": 1733747205,
"narHash": "sha256-8BRnYXnl0exUL/sRD2I382KHiY5TKWzVBQw6+6YO4yw=",
"owner": "rvolosatovs",
"repo": "nix-log",
"rev": "354b9acbdb08a5567a97791546c1e23c9f476ef6",
"type": "github"
},
"original": {
"owner": "rvolosatovs",
"repo": "nix-log",
"type": "github"
}
},
"nixify": {
"inputs": {
"advisory-db": "advisory-db",
"crane": "crane",
"fenix": "fenix",
"flake-utils": "flake-utils",
"macos-sdk": "macos-sdk",
"nix-filter": "nix-filter",
"nix-log": "nix-log",
"nixlib": "nixlib_3",
"nixpkgs-darwin": "nixpkgs-darwin",
"nixpkgs-nixos": "nixpkgs-nixos",
"rust-overlay": "rust-overlay_2"
},
"locked": {
"lastModified": 1743007747,
"narHash": "sha256-wrntf3gtM1V0eppzGlYCI+tcwjhjLkT1aKCvetmQ5ks=",
"owner": "rvolosatovs",
"repo": "nixify",
"rev": "7da7a12dfe45bbb6d339b810a6c4ef0a02a79b82",
"type": "github"
},
"original": {
"owner": "rvolosatovs",
"repo": "nixify",
"type": "github"
}
},
"nixify_2": {
"inputs": {
"crane": "crane_2",
"fenix": "fenix_2",
"flake-utils": "flake-utils_2",
"nix-filter": "nix-filter_2",
"nixlib": "nixlib",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1679748566,
"narHash": "sha256-yA4yIJjNCOLoUh0py9S3SywwbPnd/6NPYbXad+JeOl0=",
"owner": "rvolosatovs",
"repo": "nixify",
"rev": "80e823959511a42dfec4409fef406a14ae8240f3",
"type": "github"
},
"original": {
"owner": "rvolosatovs",
"repo": "nixify",
"type": "github"
}
},
"nixlib": {
"locked": {
"lastModified": 1679187309,
"narHash": "sha256-H8udmkg5wppL11d/05MMzOMryiYvc403axjDNZy1/TQ=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "44214417fe4595438b31bdb9469be92536a61455",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixlib_2": {
"locked": {
"lastModified": 1679791877,
"narHash": "sha256-tTV1Mf0hPWIMtqyU16Kd2JUBDWvfHlDC9pF57vcbgpQ=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "cc060ddbf652a532b54057081d5abd6144d01971",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixlib_3": {
"locked": {
"lastModified": 1742692082,
"narHash": "sha256-s3XOULQj7BVO7myY5V4Sob0tRZ7nRpwEOIzXg/MkD/Q=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "a09310bc940f245e51b1ffea68731244ca38f2bd",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1679577639,
"narHash": "sha256-7u7bsNP0ApBnLgsHVROQ5ytoMqustmMVMgtaFS/P7EU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "8f1bcd72727c5d4cd775545595d068be410f2a7e",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-22.11-darwin",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-darwin": {
"locked": {
"lastModified": 1742700192,
"narHash": "sha256-UlGFMAX7kGEH77qE79ZNC1N7DGBEhH+eMMbGQtvzUes=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "e8a44f32c46444d45482e6f962b52dc21ba91281",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-24.11-darwin",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-nixos": {
"locked": {
"lastModified": 1742751704,
"narHash": "sha256-rBfc+H1dDBUQ2mgVITMGBPI1PGuCznf9rcWX/XIULyE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "f0946fa5f1fb876a9dc2e1850d9d3a4e3f914092",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixify": "nixify"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1742296961,
"narHash": "sha256-gCpvEQOrugHWLimD1wTFOJHagnSEP6VYBDspq96Idu0=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "15d87419f1a123d8f888d608129c3ce3ff8f13d4",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"rust-analyzer-src_2": {
"flake": false,
"locked": {
"lastModified": 1679520343,
"narHash": "sha256-AJGSGWRfoKWD5IVTu1wEsR990wHbX0kIaolPqNMEh0c=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "eb791f31e688ae00908eb75d4c704ef60c430a92",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": [
"nixify",
"nix-log",
"nixify",
"flake-utils"
],
"nixpkgs": [
"nixify",
"nix-log",
"nixify",
"nixpkgs"
]
},
"locked": {
"lastModified": 1679537973,
"narHash": "sha256-R6borgcKeyMIjjPeeYsfo+mT8UdS+OwwbhhStdCfEjg=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "fbc7ae3f14d32e78c0e8d7865f865cc28a46b232",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"rust-overlay_2": {
"inputs": {
"nixpkgs": [
"nixify",
"nixpkgs-nixos"
]
},
"locked": {
"lastModified": 1742870002,
"narHash": "sha256-eQnw8ufyLmrboODU8RKVNh2Mv7SACzdoFrRUV5zdNNE=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "b4c18f262dbebecb855136c1ed8047b99a9c75b6",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

27
flake.nix Normal file
View file

@ -0,0 +1,27 @@
{
inputs.nixify.url = "github:rvolosatovs/nixify";
outputs = {nixify, ...}:
nixify.lib.rust.mkFlake {
src = ./.;
buildOverrides = {
pkgs,
pkgsCross ? pkgs,
...
}: {
buildInputs ? [],
depsBuildBuild ? [],
...
}:
with pkgs.lib; {
buildInputs =
buildInputs
++ optional pkgs.stdenv.hostPlatform.isDarwin pkgs.libiconv;
depsBuildBuild =
depsBuildBuild
++ optional pkgsCross.stdenv.hostPlatform.isDarwin pkgsCross.xcbuild.xcrun;
};
};
}

212
src/main.rs Normal file
View file

@ -0,0 +1,212 @@
use bevy::prelude::*;
use std::time::Duration;
const WINDOW_WIDTH: f32 = 600.0;
const WINDOW_HEIGHT: f32 = 800.0;
const PLAYER_SPEED: f32 = 300.0;
const BULLET_SPEED: f32 = 500.0;
const ENEMY_SPEED: f32 = 100.0;
#[derive(Component)]
struct Player {
speed: f32,
shoot_cooldown: Timer,
}
#[derive(Component)]
struct Bullet;
#[derive(Component)]
struct Enemy;
#[derive(Resource)]
struct EnemySpawnTimer {
timer: Timer,
}
fn main() {
App::new()
.insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0)))
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Galaga".into(),
resolution: (WINDOW_WIDTH, WINDOW_HEIGHT).into(),
..default()
}),
..default()
}))
.add_systems(Startup, setup)
.add_systems(Update, (
move_player,
player_shoot,
move_bullets,
move_enemies,
check_bullet_collisions,
spawn_enemies,
))
.insert_resource(EnemySpawnTimer {
timer: Timer::new(Duration::from_secs_f32(0.5), TimerMode::Repeating),
})
.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
// Spawn player
commands.spawn((
SpriteBundle {
sprite: Sprite {
color: Color::rgb(0.0, 0.5, 1.0),
custom_size: Some(Vec2::new(30.0, 30.0)),
..default()
},
transform: Transform::from_translation(Vec3::new(0.0, -300.0, 0.0)),
..default()
},
Player {
speed: PLAYER_SPEED,
shoot_cooldown: Timer::new(Duration::from_secs_f32(0.5), TimerMode::Once),
},
));
}
fn move_player(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut query: Query<(&mut Transform, &Player)>,
time: Res<Time>,
) {
if let Ok((mut transform, player)) = query.get_single_mut() {
let mut direction = 0.0;
if keyboard_input.pressed(KeyCode::KeyA) || keyboard_input.pressed(KeyCode::ArrowLeft) {
direction -= 1.0;
}
if keyboard_input.pressed(KeyCode::KeyD) || keyboard_input.pressed(KeyCode::ArrowRight) {
direction += 1.0;
}
transform.translation.x += direction * player.speed * time.delta_seconds();
transform.translation.x = transform.translation.x.clamp(-WINDOW_WIDTH / 2.0 + 15.0, WINDOW_WIDTH / 2.0 - 15.0);
}
}
fn player_shoot(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut query: Query<(&Transform, &mut Player)>,
mut commands: Commands,
time: Res<Time>,
) {
if let Ok((transform, mut player)) = query.get_single_mut() {
player.shoot_cooldown.tick(time.delta());
if (keyboard_input.just_pressed(KeyCode::Space) || keyboard_input.just_pressed(KeyCode::ArrowUp))
&& player.shoot_cooldown.finished() {
// Reset cooldown
player.shoot_cooldown.reset();
// Spawn bullet
commands.spawn((
SpriteBundle {
sprite: Sprite {
color: Color::rgb(1.0, 1.0, 1.0),
custom_size: Some(Vec2::new(5.0, 15.0)),
..default()
},
transform: Transform::from_translation(Vec3::new(
transform.translation.x,
transform.translation.y + 20.0,
0.0
)),
..default()
},
Bullet,
));
}
}
}
fn move_bullets(
mut query: Query<(Entity, &mut Transform), With<Bullet>>,
time: Res<Time>,
mut commands: Commands,
) {
for (entity, mut transform) in query.iter_mut() {
transform.translation.y += BULLET_SPEED * time.delta_seconds();
// Despawn bullets that go out of screen
if transform.translation.y > WINDOW_HEIGHT / 2.0 {
commands.entity(entity).despawn();
}
}
}
fn spawn_enemies(
mut commands: Commands,
time: Res<Time>,
mut timer: ResMut<EnemySpawnTimer>,
enemy_query: Query<&Enemy>,
) {
timer.timer.tick(time.delta());
// Only spawn if timer finished and we don't have too many enemies
if timer.timer.just_finished() {
let x_pos = (fastrand::f32() - 0.5) * (WINDOW_WIDTH - 40.0);
commands.spawn((
SpriteBundle {
sprite: Sprite {
color: Color::rgb(1.0, 0.2, 0.2),
custom_size: Some(Vec2::new(40.0, 40.0)),
..default()
},
transform: Transform::from_translation(Vec3::new(
x_pos,
WINDOW_HEIGHT / 2.0 - 20.0,
0.0
)),
..default()
},
Enemy,
));
}
}
fn move_enemies(
mut query: Query<(Entity, &mut Transform), With<Enemy>>,
time: Res<Time>,
mut commands: Commands,
) {
for (entity, mut transform) in query.iter_mut() {
transform.translation.y -= ENEMY_SPEED * time.delta_seconds();
// Despawn enemies that go out of screen
if transform.translation.y < -WINDOW_HEIGHT / 2.0 {
commands.entity(entity).despawn();
}
}
}
fn check_bullet_collisions(
mut commands: Commands,
bullet_query: Query<(Entity, &Transform), With<Bullet>>,
enemy_query: Query<(Entity, &Transform), With<Enemy>>,
) {
for (bullet_entity, bullet_transform) in bullet_query.iter() {
for (enemy_entity, enemy_transform) in enemy_query.iter() {
let bullet_pos = bullet_transform.translation;
let enemy_pos = enemy_transform.translation;
// Simple collision detection using distance
let distance = bullet_pos.distance(enemy_pos);
if distance < 25.0 { // Approximate collision radius
// Despawn both bullet and enemy on collision
commands.entity(bullet_entity).despawn();
commands.entity(enemy_entity).despawn();
break;
}
}
}
}