Computronium - [Grey Matter Games]

My internship at Grey Matter Games


Project Goal

Computronium is an automation game based on building and managing a retro sci-fi datacenter.
Currently in development.
It's designed to grant flexible multiplayer and full mod support to try to strive for a community first approach for both self-advertisement and user-created content.

Grey Matter Games is a small emerging company founded by industry veterans with a great interest for open-source development and a focus on longer development cycles.

You can find out more about the game on the official Computronium Steam Page.

My Contributions

I joined the company as an intern during the Spring 2025 semester.
In my intership, I embraced a generalist butterfly role, working across various fields and task, both trying to learn as much as possible, and to give back to the project my own contribution.





UI / UX

Th general mood of the game was retro-futuristic, as such the UI was immagined to be inspired to analogic interfaces, minimal, industrial, and fitting for a sci-fi datacenter.

One of the compnay's objective was accomodate as many user preference as possible, from different screen sizes and input devices to extra modded features. For this reason, all menus were designed to be indefinitely expandable, allowing new options and features to be added without requiring a complete redesign of the interface.

Since the company is a strong supporter of open-source software, both the UI and UX work were created using Penpot and Affine , keeping the design process aligned with the project's open-source philosophy. Affine was used as moodboard platform to gather and share reference images and concepts similar to how Miro is used, while Penpot was used to create interactive prototypes of UI interactions simillar to Canvas.

At this point in development the game was far from being able to implement any of the UI directly. Therefore my task was to design an eligible prototype and document them clearly, so that future developers could implement them eventually.

As such, the images here represent some of the UI prototypes I created, along with explanatory illustrations detailing features and the reasoning behind some of the design decisions.



Task UI -1



Engine Development

This was my first time studying and operating using the Rust programming language and also Bevy, the open source engine built in Rust that Computronium is based on. This alone was a notable challenge, as Rust resulted a much more complex programming language than than those I had worked with previously.
Meanwhile Bevy itself resulted pretty straightforward to learn and use, as far as a viewport-less engine can be.
With help from the development team, I implemented several reusable, general purpose systems, as well as more direct gameplay features.
All the contributions followed one of the game's core design pillars: multiplayer-first approach. This meant testing features from the perspective of multiple users to ensure that they worked correctly on both clients and servers.
Another core pillar was full mod support therefore all features implemented in game were designed with extensibility in mind, allowing additional functionality to be added through mods without requiring the original systems to be rewritten.
I also implemented the first sounds in the game, starting from SFX I created specifically for the project and from music produced by the company's composer. These sounds were implemented using Bevy's native audio system.
Other smaller features were added to the engine, such as a location ping system to communicate between players, as well as a debug mode for visualizing the locations of spawned sounds, which was used to test sound spatialization.

Sound System Implementation

Here is shown the full sound class I created and used during my internship,
used for all the sounds and with the implemented features

									
use crate::prelude::*;

#[cfg(feature = "client")]
pub(super) mod client_only {
    use bevy::audio::SpatialScale;
    use bevy_tnua::prelude::*;
    use bevy_tnua::builtins::TnuaBuiltinJumpState;
    use rand::prelude::*;
    use bevy::color::palettes::basic;

    use super::*;

    const SPATIAL_SCALE_VALUE : f32 = 0.05;
    const SPATIAL_SCALE : SpatialScale = SpatialScale::new(SPATIAL_SCALE_VALUE);

    pub fn plugin(app: &mut App) {
        app.add_systems(FixedUpdate, player_jump_observer);

        app.add_observer(spawn_foundation_observer);
        app.add_observer(spawn_ambience_observer);
        app.add_observer(spawn_chassis_observer);
        app.add_observer(spawn_wall_observer);
        app.add_observer(spawn_ping_observer);
        app.add_observer(spawn_server_observer);
        app.add_observer(spawn_music_observer);
        app.add_observer(sound_setup);
    }

    fn make_sound_gizmo() -> GizmoAsset {
        let mut gizmo = GizmoAsset::default();
        gizmo
            .sphere(Isometry3d::default(), 1., basic::YELLOW)
            .resolution(4);

        gizmo
    }

    fn sound_setup(
        trigger: Trigger,
        asset_server: Res,
        mut commands:Commands,

    ) {
        //space between two ears
        let gap = -5.0;
        let listener = SpatialListener::new(gap);
        let parent_entity = trigger.target();
        let listener_entity = commands.spawn((
        Transform::default(),
        listener,
        )).id();

        commands.entity(parent_entity).add_child(listener_entity);
    }

    //TODO: (Giorgio) use something better than the camera component
    fn spawn_ambience_observer(
        trigger: Trigger,
        mut commands: Commands,
        asset_server: Res,
    ) {
        let sound_effect: Handle = asset_server.load("audio/sfx/WindLooping.ogg");
        commands.spawn((
            AudioPlayer(sound_effect),
            PlaybackSettings::LOOP,));
    }

    fn spawn_wall_observer(
        trigger: Trigger,
        mut commands: Commands,
        asset_server: Res,
        mut gizmo_assets: ResMut>,
        toggles: Res,
    ) {
        //Play a randomly selected sound
        let rng = &mut rand::rng();
        let sound_paths = [
            "audio/sfx/Crush5.ogg",
            "audio/sfx/Build1.ogg",
            "audio/sfx/Build2.ogg",
        ];
        let Some(path) = sound_paths.choose(rng) else {
            fail!("Unable to load sound file from list");
            return;
        };
        let sound_effect: Handle = asset_server.load(*path);
        let parent_entity = trigger.target();

        //spawn sound
        let mut sound = commands.spawn((
            AudioPlayer(sound_effect),
            PlaybackSettings::DESPAWN.with_spatial(true).with_spatial_scale(SPATIAL_SCALE),
            ChildOf(parent_entity),
            Transform::default(),
        ));

        if toggles.sound_gizmo
        {
            let gizmo = make_sound_gizmo();
            sound.insert(
                children![(
                    Gizmo {
                        handle: gizmo_assets.add(gizmo),
                        ..default()
                    },
                )],
            );
        }
    }

    fn spawn_server_observer(
        trigger: Trigger,
        mut commands: Commands,
        asset_server: Res,
        mut gizmo_assets: ResMut>,
        toggles: Res,
    ) {
        let sound_effect: Handle = asset_server.load("audio/sfx/Wood1.ogg");
        let parent_entity = trigger.target();

        //spawn sound
        let mut sound = commands.spawn((
            AudioPlayer(sound_effect),
            PlaybackSettings::DESPAWN.with_spatial(true).with_spatial_scale(SPATIAL_SCALE),
            ChildOf(parent_entity),
            Transform::default(),
        ));
        if toggles.sound_gizmo
        {
            let gizmo = make_sound_gizmo();
            sound.insert(
                children![(
                    Gizmo {
                        handle: gizmo_assets.add(gizmo),
                        ..default()
                    },
                )],
            );
        }
    }

    //TODO: (Giorgio) use something better than the camera component
    fn spawn_music_observer(
        trigger: Trigger,
        mut commands: Commands,
        asset_server: Res,
    ) {
        let sound_effect: Handle = asset_server.load("audio/sfx/Music/Frontier 1 v_1A OGG.ogg");
        commands.spawn((
            AudioPlayer(sound_effect),
            PlaybackSettings::LOOP,));
    }

    fn spawn_foundation_observer(
        trigger: Trigger,
        mut commands: Commands,
        asset_server: Res,
        mut gizmo_assets: ResMut>,
        toggles: Res,
    ) {
        //Play a randomly selected sound
        let rng = &mut rand::rng();
        let sound_paths = [
            "audio/sfx/Crush5.ogg",
            "audio/sfx/Build1.ogg",
            "audio/sfx/Build2.ogg",
        ];
        let Some(path) = sound_paths.choose(rng) else {
            fail!("Unable to load sound file from list");
            return;
        };
        let sound_effect: Handle = asset_server.load(*path);
        let parent_entity = trigger.target();

        //spawn sound
        let mut sound= commands.spawn((
            AudioPlayer(sound_effect),
            PlaybackSettings::DESPAWN.with_spatial(true).with_spatial_scale(SPATIAL_SCALE),
            ChildOf(parent_entity),
            Transform::default(),
            ));
        if toggles.sound_gizmo
        {
            let gizmo = make_sound_gizmo();
            sound.insert(
                children![(
                    Gizmo {
                        handle: gizmo_assets.add(gizmo),
                        ..default()
                    },
                )],
            );
        }
    }

    fn spawn_chassis_observer(
        trigger: Trigger,
        mut commands: Commands,
        asset_server: Res,
        mut gizmo_assets: ResMut>,
        toggles: Res,
    ) {
        let sound_effect: Handle = asset_server.load("audio/sfx/Slide3.ogg");
        let parent_entity = trigger.target();

        //spawn sound
        let mut sound = commands.spawn((
            AudioPlayer(sound_effect),
            PlaybackSettings::DESPAWN.with_spatial(true).with_spatial_scale(SPATIAL_SCALE),
            ChildOf(parent_entity),
            Transform::default(),
            ));
        if toggles.sound_gizmo
        {
            let gizmo = make_sound_gizmo();
            sound.insert(
                children![(
                    Gizmo {
                        handle: gizmo_assets.add(gizmo),
                        ..default()
                    },
                )],
            );
        }
    }

    fn spawn_ping_observer(
        trigger: Trigger,
        mut commands: Commands,
        asset_server: Res,
        mut gizmo_assets: ResMut>,
        toggles: Res,
    ) {
        let sound_effect: Handle = asset_server.load("audio/sfx/Pew1.ogg");
        let parent_entity = trigger.target();

        //spawn sound
        let mut sound = commands.spawn((
            AudioPlayer(sound_effect),
            PlaybackSettings::DESPAWN.with_spatial(true).with_spatial_scale(SpatialScale::new(SPATIAL_SCALE_VALUE/3.0)),
            ChildOf(parent_entity),
            Transform::default(),
            ));
        if toggles.sound_gizmo
        {
            let gizmo = make_sound_gizmo();
            sound.insert(
                children![(
                    Gizmo {
                        handle: gizmo_assets.add(gizmo),
                        ..default()
                    },
                )],
            );
        }
    }

    fn spawn_connection_observer(
        //TODO: (Giorgio) connect this sound when the connection logic is implemented in the gameplay
        //trigger:Trigger,
        mut commands: Commands,
        asset_server:Res,
        mut gizmo_assets: ResMut>,
        toggles: Res,
    ) {
        let sound_effect:Handle = asset_server.load("audio/sfx/PlugIn1.ogg");
        //let parent_entity = trigger.target();

        //spawn sound
        let mut sound = commands.spawn((
            AudioPlayer(sound_effect),
            PlaybackSettings::DESPAWN.with_spatial(true).with_spatial_scale(SPATIAL_SCALE),
            //ChildOf(parent_entity),
            Transform::default(),
            ));
        if toggles.sound_gizmo
        {
            let gizmo = make_sound_gizmo();
            sound.insert(
                children![(
                    Gizmo {
                        handle: gizmo_assets.add(gizmo),
                        ..default()
                    },
                )],
            );
        }
    }

    fn despawn_connection_observer(
        //TODO: (Giorgio) connect this sound when the connection logic is implemented in the gameplay
        //trigger:Trigger,
        mut commands: Commands,
        asset_server:Res,
        mut gizmo_assets: ResMut>,
        toggles: Res,
    ) {
        let sound_effect:Handle = asset_server.load("audio/sfx/PlugOut1.ogg");
        //let parent_entity = trigger.target();

        //spawn sound
        let mut sound = commands.spawn((
            AudioPlayer(sound_effect),
            PlaybackSettings::DESPAWN.with_spatial(true).with_spatial_scale(SPATIAL_SCALE),
            //ChildOf(parent_entity),
            Transform::default(),
            ));
        if toggles.sound_gizmo
        {
            let gizmo = make_sound_gizmo();
            sound.insert(
                children![(
                    Gizmo {
                        handle: gizmo_assets.add(gizmo),
                        ..default()
                    },
                )],
            );
        }
    }

    fn player_jump_observer(
        controller_query: Query<(Entity, &TnuaController)>,
        mut commands: Commands,
        asset_server: Res,
        mut gizmo_assets: ResMut>,
        toggles: Res,
    ) {
        for (parent_entity, controller) in controller_query.iter() {
            if let Some(TnuaBuiltinJump::NAME) = controller.action_name() {
                let (_, jump_state) = controller
                    .concrete_action::()
                    .expect("action name mismatch");

                if let TnuaBuiltinJumpState::StartingJump {..} = jump_state {
                    //Play a randomly selected sound
                    let rng = &mut rand::rng();
                    let sound_paths = [
                        "audio/sfx/Crush3.ogg",
                    ];

                    let Some(path) = sound_paths.choose(rng) else {
                        fail!("Unable to load sound file from list");
                        continue;
                    };

                    let sound_effect: Handle = asset_server.load(*path);

                    //spawn sound
                    let mut sound = commands.spawn((
                        AudioPlayer(sound_effect),
                        PlaybackSettings::DESPAWN.with_spatial(true).with_spatial_scale(SPATIAL_SCALE),
                        Transform::default(),
                        ChildOf(parent_entity),
                        ));
                    if toggles.sound_gizmo
                    {
                        let gizmo = make_sound_gizmo();
                        sound.insert(
                            children![(
                                Gizmo {
                                    handle: gizmo_assets.add(gizmo),
                                    ..default()
                                },
                            )],
                        );
                    }
                }
            }
        }
    }
}


									
								

At the end of my internship, the last thing I did was implementing Bevy's athmospherics, who, at the time, were just added in the newer bevy release version 1.16.
Initially I made this board for myself to test out various settings and brainstorm which ones could work in an alient planet environment. Then I decided to make this board accessible as part of the project documentation, both to show my process and to give the other developers an overview of how I approached this task.

In retrospect however, this board perhaps aged poorly, since Bevy's atmospherics have since changed significantly in both functionality and implementation. ecause of this, it is no longer particularly useful as a reference for current versions of Bevy.
But I have kept it here as documentation of my experience, experimentation, and approach at the time.




Sound Design & Implementation

During my internship, I worked on both the design and implementation of the game's very first sound effects. This included creating and editing sound assets using Reaper, implementing them into the game using Bevy's audio system.

I also developed several features to support the sound implementation process. These included spatial audio, randomized sound variations, looping tracks, and a debug visualization tool that displayed the position of spawned sounds in the game world. This was especially useful when testing and fine-tuning spatialized sounds.

The video showcases a small selection of the sound effects I worked on, along with spacialization and the sound visual debugger. It also shows other features I worked on during the internship, such as the player ping system. Additional sounds were created and implemented that are not shown in the video.

The full source code realised to implement the sound is shown in Engine Development .




More Projects -

See all projects