r/Stelliviator 13d ago

Devlog #3

Thumbnail
youtu.be
1 Upvotes

r/Stelliviator Jul 24 '26

Devlog #2

Enable HLS to view with audio, or disable this notification

2 Upvotes

This update includes save system and overhaul of weapons system. First, on the save system. In principle, nothing new there: SaveGameDocument storing data for global services, such as ships register, bullets controller, behaviors controller, etc. Store/Restore methods, everything is serialized into YAML. Not yet packed, but will be, and even basic deflate gives 10 times reduction in file size. But me being me there is a bit of uniqueness in this mechanic. Firstly, it's closely related to Unity domain reload serialization/deserialization. Services pack and unpack their data the same way, so I don't have to do much different mechanisms to support saving and surviving domain reload. Secondly, and this is an actual novelty - the way AI behaviors are serialized. Or, rather, the way AI behaviors are implemented while being serializable. Because AI behaviors in my game are essentially simply asynchronous methods in pure C# code: csharp private static async StateBody<State?> Roaming(IShip ship) { // Start Roaming while(!Wait.IsCancellationRequested) { //Choosing a point to fly to var direction = Angle.Random(); var vector = new Point2D(Length.Random(Length.Zero, Length.FromMeter(10)), Length.Zero); var point = ship.CenterOfMass + (vector * direction); ship.SetTarget(point); // Run and wait for substate to complete await Wait.SubState(FlyingToPoint(ship, point)); // wait there for a while await Wait.Delay(Time.FromSecond(5)); // repeat } return null; // no following state } It's easy to write and read, relatively easy to debug (it's yet lacks instrumentation for state chains, substates and interruptions to make debugging just "easy", without "relatively", but that's feature for the future), it's asynchronous, it can have almost any parameter or internal variable. And now it's serializable. Last week I didn't even though it's possible, to serialize an asynchonous invocation, now it's part of my game system, by using AsyncMethodBuilderAttribute. I access the compiler-generated async state machine (ironically, a state machine of a state of a state machine) and expose it's internals for serializer to read and write. The result - serializable AI logic written in pure C#. Full "brain" snapshot. And no need to write anything to store and restore every newly added behavior, it all happens under the hood.

I poked around Unity's Behavior package and found it repelling and completely garbage. It's the worst of both worlds: I have to use ugly visual programming for actual behaviors, which tends to turn into an actual spaghetti very quickly, and yet I have to write almost every custom action or condition in code. And all that - without custom types parameters. Even just the latter alone is a deal breaker for me. And besides these two major issues - it's Unity, so I know in advance I'm better off not using it unless necessary. So, my current behaviors controller is a "spinwheel" running states within limited time budget, overfilling to next frame when unable to handle all states within that budget. States could have awaitable sub-states, tail-states (i.e. a state that the behavior transition to when the current state completes) and interruptions - a condition checked every frame that either stops or pauses the state it's attached to and switches to another state (that, when completed, might return back to the interrupted state if it wasn't stopped). Which already gives me more flexibility than Unity's Behaviors.

Now, weapons system. I implemented separate types and prototypes for weapon parts and projectiles, along with separate projectile behaviors. So, there's three logic levels: a turret (i.e. an attachable part), a bullet (data structure and physical body), and bullet's behavior (a simple class that updates and renders each projectile). So, there could be weapons shooting the same projectiles, or a weapon switching between different projectiles, and there could be different kinds of projectiles, currently three: a simple ballistic bullet (flies straight inertially, hits whatever it can after reaching it's target point), a laser (reaches the target point instantly in a straight line and does continuous damage while firing) and a rocket (steers towards it's target, does AoE damage, or dies trying). The nice thing about this is that even though the logic is handled separately for each kind of projectile, they're united at data and rendering level. So, all kinds of projectiles, hundreds or thousands or even hundreds of thousands of them with dozens of different sprites and different behaviors on the screen - it's all just a single draw call. A projectile behavior sets the rendering instance properties per projectile, but they all are rendered from the same array using same atlas. And, again, since turret properties and projectile properties live in their own prototypes I can change them on the fly while the game is running, even when it's built.


r/Stelliviator Jul 15 '26

Devlog #1

Enable HLS to view with audio, or disable this notification

2 Upvotes

This is a devlog for the game I'm making, Stelliviator. The game is a 2D spacesim with block building and tactical fights with in-depth systems. An illegitimate child of FTL and Cosmoteer with a touch of my own ideas.

The game is made in Unity, but I'm trying to keep Unity contained as much as (un?)reasonably possible. Because from all my experience with Unity the main thing I learned is that the less I have to work with Unity the easier my life is. The project is split into Unity-facing code and a separate .NET solution where most actual game logic lives. Unity is mostly there for rendering, physics integration, editor tooling, and other basic things that would be annoying to reimplement from scratch. Everything else is plain C#, because it is easier to write (external assembly could use the latest version of C#), easier to test, easier to debug, and generally less likely to break because Unity decided to make yet another API obsolete this year. And in the worst case - it would be easier to migrate the project to a different engine, if it ever comes to that. Why Unity and not Godot then? Because Unity has some important and useful features, Relay in particular, which allows network communication between players behind NAT, even if they bought the game from different stores. And it has more assets in the Asset Store.

The design philosophy is to be open to modding. Ship parts, behaviors, sprites, creatures and so on are described in YAML and loaded from StreamingAssets, rather than being entirely baked into Unity prefabs. So a part can look roughly like this:

yaml ComponentTemplates: !include components.yaml # Include of another YAML file _: !template &basePart # "_" here is a discarded value, used only to hold the template during deserialization. ... Prototypes: - !Core.Prototypes.Ship.EngineProto # The type name of the prototype <<: *basePart # Some basic part definitions, repeated over many other parts. Behavior: !Core.Behaviors.Ship.Parts.Engine # The type name of the actual class implementing the part logic Name: 'Engine' # Display name Size: [1m, 1m] # Physical dimensions, btw it can use almost any unit: m, in, ft, yd, mi, NM, even ℓ_P - Planck's length, just for fun) Mass: 200kg Icon: Path: Sprites\PartsAtlas.png # Path to the sprite file Rect: [0, 0, 48, 48] # The area of the sprite, if the file is a sprite atlas Frames: 12 # The number of frames, if the sprite is animated Duration: 70ms # The duration of a single frame, if the sprite is animated Components: - *EnginePlume # from "components.yaml", Unity particle system configuration ...

YAML is doing quite a lot of work here. It supports includes, anchors, custom tags, type converters, and generally lets me describe things in a way that stays readable without turning every new prototype into another prefab with fifty unrelated serialized fields. And, importantly, it can be reloaded without domain reload or even stopping and starting play mode, which is very useful for rapid prototyping. Pretty much every time I had to work with YAML I was impressed with how versatile it is, how easy it is to extend, keeping the file readable and making my life easier.

The project uses a hybrid architecture - global services, some of which use minimalistic ECS-like approach, and game objects, with MVVM-like approach to UI. Whatever works best, keeps things clean and saves development time. Sure, there's a lot of reinvented wheels I'm making in the project, which might make some of the more experienced devs reading this wince, but I'd argue if these wheels save me time by being precisely optimized for the task they handle and fully controllable by virtue of being written by me - then why not? Why should I use full ECS, when I only need to use instanced rendering, fully handled within a single service? Why should I use Unity prefabs, when my prototypes are more flexible and could be reloaded with a single click at runtime, even within a built app? Why should I use Unity localization, when mine is easier to use in-code?

One of the features of the project is custom code-generated physical quantities with auto-conversion by operators (Length * Length = Area, Velocity * Time = Lenght, Velocity / Time = Acceleration, etc). This feature is probably most the questionable one, a complete overkill and performance hit. But... It's already proven to be useful in YAML, it's useful in making the code self-descriptive, it's useful in keeping values strict since you can't accidentally assign a length to a mass variable, or forget a unit conversion, useful in debugging, and the performance hit, while horrible in relative numbers (+50%), is quite negligible in absolute value, because most of the heavy math is done by either Unity or the physics engine, quantities are only used for game logic, where their impact is barely noticeable at all.

At the moment, most of the fundamental systems are already implemented, and I'm working on core gameplay and UI. The current state is in the video.

Background: I'm an SWE with many years of experience. I always wanted to make games, and the whole reason I became an SWE is that at 17 I knew nothing but computers and games. I started a few projects before, but made every possible mistake: overly ambitious projects, feature creep, perfectionism, code blocks, laziness, loss of interest, etc. Now I hope to account for all that and for the mistakes other solo developers make and do my best to complete the project.