Skip to content
arbitrary hexcode edited this page Feb 25, 2024 · 16 revisions

The Vision

In space you fly around to explore planets and star systems, and combat other ships. Upgrade your ship or swap out components for different abilities.

In space there are asteroids floating around. Asteroids spawn in belts around stars. Or fields floating through space. Similar to original arcade style asteroids. Arbitrary convex polygons, but shatter in such a way that all the pieces that break off can make up the original object. To achieve this; voronoi diagrams! A cool property of voronoi diagrams is that the polygons it creates are always convex by nature. When an asteroid is shot by player, create a set of random points (concentrated where the bullet hits) within the asteroids polygon. From those points make a voronoi diagram within the original polygon, then break up the asteroid into the resulting pieces. Those pieces can also be hit and broken down further with the same process. Until a certain threshold of smallness where we actually destroy the object because there's no point in breaking it down further. Upon destruction, chance to drop items/resources for the player to pick up.

Players can land on planets. On the planet the player will be able to build their base. Mine for resources. Build/upgrade their ship. Planets are generated by a tile-able noise so that the edges of the map meet. The goal is to have a toroidal planet with a finite 2D map where you can walk continuously in one direction and eventually come back around to where you started. This is achieved via OpenSimplexNoise, a bit of trig and modulus magic for rendering! But there are some other technical challenges this presents that I have not yet addressed. see https://simonschreibt.de/gat/1nsane-carpet-2-repetitive-worlds/

Currently space is fairly empty. Worlds are very empty. Just test objects while I flesh out my ideas and prototype systems.

MILESTONES:

  • started off with basic POC in a double-buffered JFrame. pure java, no libraries.
  • very basic physics sim with separate position, velocity, acceleration in a 0G environment.
  • basic polygon asteroids with simple AABB collision detection
  • lose the original source because I don't understand git :p
  • realize engines are hard, cry and move to libGDX
  • implement simple custom ECS
  • re-implement simple spaceship flight and basic asteroid polygons in new ECS style
  • collisions support for rotated bounding box with convex polygons
  • procedurally generated ship textures
  • basic space render system with orbiting bodies
  • 3D renderer for rotating 2D textures on the third axis
  • world render system with tiled noise
  • engine architecture that supports transfer between space and planets
  • better ECS: migrate to ashley
  • moved to B2D: fixed physics step and collision detection with impulse resolution
  • pause menu
  • title screen
  • custom shape renderer to draw filled polygons for asteroids
  • basic asteroid destruction using Delaunay triangulation
  • simple shader for star textures
  • basic sfx
  • lazers!!!
  • better asteroid destruction using voronoi

Engine Architecture

This game is written in Java, GLSL for shaders, and built on top of libGDX. libGDX is a cross-platform Java game development framework based on OpenGL (ES) that works on Windows, Linux, macOS, Android, your browser and iOS.

The engine architecture is designed around an Entity Component System. Generally:

  • Entities are composed of Components.
  • Systems operate on Components.
  • Systems are managed by the Engine.

Most of the game logic and rendering happens in systems. For simplicity and consistency, components are pure data. No implementation.

To get started with libGDX: https://libgdx.com/wiki/

To get started with the Ashley ECS framework: https://github.com/libgdx/ashley/wiki/Framework-overview

To get started with the Box2D Physics engine: https://box2d.org/documentation/

To get started with VisUI scene2D UI: https://github.com/kotcrab/vis-ui

Project Structure

ECS layout: [...\SpaceProject\core\src\com\spaceproject]

    \systems: all systems go in here
    \components: all components go in here
    \utility
        Mappers: Component Mapper Boilerplate
        ECSUtil: Some tools for managing and debugging entities
        SystemLoader: load and unload systems based on SystemsConfig

Systems are dynamically loaded and unloaded per context by the SystemLoader, as defined by SystemsConfig:

    Space and World can have different systems.
        eg: don't need an orbit system when on a planet.
    Mobile and Desktop can have different systems.
        eg: don't need to render touch control on desktop

Data Directory [...\SpaceProject\assets]

    \config:
        One goal is to make the engine highly tune-able/configurable. (perhaps I have gone too far in this direction at the cost of complexity?)
        Plain text JSON files that define all values for game. These files will be written on first run of the game.
        if no config exists, default is loaded and written to disk. If config exists on disk, it is loaded.
        [default always loaded when debugDevForceLoadDefault = true]
        SystemsConfig: define system loading priorities and properties.
            priority: order of execution is important for game loop logic and rendering pipeline.
            haltOnGamePause: if true, system stops processing when game is paused.
            loadInSpace: if true, system will be loaded when player is in space.
            loadInWorld: if true, system will be loaded when player is on a planet.
            loadOnDesktop: if true, will be loaded on desktop platform.
            loadOnMobile: if true, will be loaded on iOS or Android platform.
        EngineConfig: define physics and rendering constants
        KeyConfig: define input keycodes
        EntityConfig: define generation parameters for entities
        CelestialConfig: defines star system generation rules
        WorldConfig: define terrain generation parameters (in progress)
        DebugConfig: defines settings like show debug rendering
        MinimapConfig: define colors and behaviors for minimap
        UIConfig: define ui element colors and positions
    \fonts: fonts for UI
        .tff   : True Type Fonts
    \shaders: GLSL shader files (name convention: reference GLSL compiler extensions)
        .vert  : vertex shader
        .frag  : fragment shader
        .glsl  : .vert.glsl, .tesc.glsl, ..., .comp.glsl compound suffixes
        .hlsl  : .vert.hlsl, .tesc.hlsl, ..., .comp.hlsl compound suffixes
    \sounds:
        Potential structure / implementation might look like: each sound effect gets it's own folder.
            - if no sound in folder, don't play
            - if one sound in folder, play that
            - if multiple sound in folder, play random
            - could allow user configurable chance? what about pitch/volume? Keep it simple...
        \music
            track a
            track b
        \effects
            \effectA
                effectA1.wav
                effectA2.wav
                effectA3.wav
            \effectB
            \effectC
    \save: (todo) serialization
        https://github.com/libgdx/libgdx/wiki/Saved-game-serialization

Sound & Music

The current upper limit for decoded audio is 1 MB. https://libgdx.com/wiki/audio/sound-effects

  • Supported Formats: MP3, OGG and WAV
    • WAV files are quite large compared to other formats
      • WAV files must have 16 bits per sample
    • OGG files don’t work on RoboVM (iOS) nor with Safari (GWT)
    • MP3 files have issues with seamless looping.
  • Continuous / Looping sound 'sound.loop()'
    • raw sound file must have no gaps
    • start and end of wave should line up to prevent clipping
  • Pitch: range = [0.5f - 2.0f] -> half to double frequency. Or +/- 1 octave from sample frequency: lower octave = [0.5 - 1.0] (eg: 440Hz * 0.5 = 220Hz) upper octave = [1.0 - 2.0] (eg: 440Hz * 2.0 = 880Hz)
  • Panning: sound must be MONO
  • Latency on android: is not great and the default implementation is not recommended for latency sensitive apps like rhythm games. https://libgdx.com/wiki/audio/audio#audio-on-android

Physics Constraints

  • Box2D uses MKS (meters, kilograms, and seconds) units, and radians for angles.
  • Body Size Limit: Keep moving objects roughly between 0.1 and 10 meters.
  • Movement limit: = 2 * units per physics step.
    • (eg step of 60: 60 * 2 = 120, max velocity = 120km/s)
    • For the [hyperdrive] feature I need to bypass this slow (relative to the size of space) limit, so the box2D physics body is disabled via Body.setActive(false);
  • The fixed physics step keeps calculations frame-rate independent. (turning vsync off won't make your ship fly faster)
  • Large world space coordinates will introduce inaccuracy, courtesy of floating point arithmetic rounding: epsilon. Floating point errors in deep space caused by large world coordinates and small frame times is affecting movement physics. Meaning the deeper in space we go, the more "stuff" begins to break. Box2D works best with world sizes less than 2 kilometers. The universe by contrast, is a little bit bigger than 2km.... To solve this, we can use a local coordinate system and a large global coordinate system.
    • todo: 'Floating Origin' -> b2World::ShiftOrigin
  • Also sprites and world units: We can't use pixels for coordinates. https://xoppa.github.io/blog/pixels/
  • Cannot remove bodies during a physics step: eg postSolve() in the physics contact listener.

https://box2d.org/documentation/md__d_1__git_hub_box2d_docs_loose_ends.html#autotoc_md126

https://box2d.org/documentation/#:~:text=Caution%3A%20Box2D%20is%20tuned%20for,between%200.1%20and%2010%20meters.

NOTE: Entities should not be removed mid-frame until all systems have finished processing! Similarly to not removing bodies until all collisions have been processed, we remove entities at end of frame so that all systems can finish processing and rendering the scene.

To remove an entity we simply add to the entity a new @RemoveComponent and entities with this component are removed at the end of the frame as the @RemovalSystem should always be the very last system fired.

entity.add(new RemoveComponent());

If a system is paranoid about execution priority and weather an entity is dead or alive, that system can check for the existence of the @RemoveComponent. (I haven't needed to yet in practice)

NOTE: The @RemovalSystem also handles memory management in that it will check the entity for data such as textures or b2D bodies and passes them off to the @ResourceDisposer to be auto-disposed. (this is likely a place to look during optimization, eg: things like bullets should be pooled)

Size of Universe & Scale

The scale of the universe is currently somewhat arbitrary as I have adjusting to the box2D velocity constraints as I implement these systems. Min/maxing the physics engine: render scale and physics bodies as small as possible to maximize the velocity limit per step. The gameplay is mainly driven by the spaceship flight, destructible asteroids, and combat. Navigating space and the control of the ship should feel intuitive and responsive in respect to the universe.

  • how big should a ship or player be in relation to the universe / planets?
  • asteroids: how big should asteroids be?
  • planets: how big should a planet be?
  • distances between planets: how long should the player spend traveling between systems and planets and stars?

Ultimately: Is flying around and shooting asteroids and other ships actually fun and satisfying? It doesn't matter how big the universe is, if the local-verse isn't fun.

Screen Transition + Dynamic System Loading & Unloading

One of the trickier parts of the engine....

I knew transitioning between space and planets would be a little tricky, memory and design wise. So after lots of researching design patterns, I knew an ECS would probably be the best tool.

ECS gives the flexibility to add or remove components during run time. This means entities can change their behavior on the fly and allows for dynamic behaviors that are completely decoupled from the entity itself.

Systems can also be loaded and unloaded on the fly. This control dynamically change the behavior of the game logic itself at runtime by loading and unloading systems. This is used to transition between space and planets. It's not "seamless", the rendering system swap is hidden behind a whiteout animation.

Player first presses transition action to land or take off:

	ControlSystem()
		if in space
			landOnPlanet()
				find planet
				add ScreenTransitionComponent -> anim stage = shrink
			takeOffPlanet()
				add ScreenTransitionComponent -> anim stage = transition

When animation hits transition state, this calls @GameScreen to begin switching context between space and worlds. @SystemLoader which loads/unloads relevant systems based on @SystemsConfig

	ScreenTransitionSystem()
		landOnPlanet:
			shrink: shrink ship sprite to give illusion of landing
			zoomIn: zoom in camera
			screenEffectFadeIn: fade screen to white
		-->	transition: <-- trigger @GameScreen to switch to planet.
			load: (todo: ensure planet map loaded before allowing play)
			screenEffectFadeOut: fade white back to screen
			pause: brief pause for effect
			exit: player get out of ship
			end: remove transition component
		takeOffPlanet:
			screenEffectFadeIn: fade screen to white
		-->	transition: <-- trigger @GameScreen to switch to space.
			sync: ensure find planet landed on is loaded (based on seed), ensure found and texture loaded before allowing play
			zoomOut: zoom camera from 0 to normal
			grow: grow ship sprite from 0 to give illusion of taking off planet
			end: remove transition component

Space Parallax Background Rendering

Background color is set based on camera positioning. Just some combinations and ratios of red, blue and green that I played around with until it looked nice. Simple but effective: 5 parallax layers:

  • stars close
  • stars far
  • stars farther
  • stars farthest
  • noise far

Noise Generation

Noise is heavy to generate at runtime so it's done in background threads.

NOTE: we cannot generate textures in another thread due to glContext...

https://libgdx.com/wiki/app/threading

https://www.opengl.org/wiki/OpenGL_and_multithreading

We can however, generate noise data for the textures on a separate thread, and then pass the data to the rendering thread. And we do that via a Threadpool. Once generated, it's picked up by @SpaceLoadingSystem who then creates a texture from the noise data. You can specify how many simultaneous threads you would like to allow in @EngineConfig.

Todo: save textures/noise so it's only generated the first time. Load from disk. For dev purposes, just generate ad-hoc for now.

AI

Currently primitive placeholder systems to flesh out ideas. They can't aim properly, need to predict where player will be. Lead target. And much more....problems for later.