πŸ€‘ Indie game storeπŸ™Œ Free gamesπŸ˜‚ Fun games😨 Horror games
πŸ‘· Game development🎨 AssetsπŸ“š Comics
πŸŽ‰ Sales🎁 Bundles

Solar Colony DevLog

A topic by InfectedBytes created 1 year ago Views: 213,880 Replies: 10
Viewing posts 1 to 10

My first idea for the jam was a 2D top-down space exploration game, where you can upgrade and modify your spaceship. But I think there will be a lot of games like this, so I came up with another idea and I think it is even better. The basic concept is much like Banished, but obviously in space ;-)

You start with a small space station and a few people. You have to mine resources, like metal and hydrogen from asteroids and/or planets. With those resources, you will be able to expand your space station.

The first name coming to my mind was "Space Colony", but there is already a game with that name (from 2003). So I have to find another name. After a few moments I came up with a new name: Solar Colony

Maybe not the best name, but it will do^^


Full DevLog

Twitter: @infected_bytes

Due to a lack of time, I wasn't able to work as much as I wanted. But at least I was able to implement some of the basic stuff.

Screens
The game consists of a stack of different ScreenStates. When starting the game, the MainMenu screen shows up and when you select a new game, the GameState is pushed onto the stack. If you pause the game, a PauseState is pushed, etc.

The top of the stack is the only state that will be updated and rendered. So if you push the pause state, the game will obviously be paused and to continue you just have to pop the pause state. Easy go.

Tilebased or "Room"-based ?
Which is the better solution? Answer: They are more or less equal.

Tilebased:
+Simple collision detection: Just check if the tiles are free!
+Super simple pathfinding
-if based on an array: limited size
-Complete rooms will be placed, but each room would consist of several tiles...meh

Room based:
+Unlimited in size
+One room is one unit
-pathfinding is a bit more complex

At the moment I prefer the later one and I hopefully stay with it! Would be a shame to change it later on...

Bad, bad design...
For an easy access, I added static access to the textures. Really ugly and bad coding style, but for a "simple" game like this one, it will work. (Don't do this at home!)

Room placement
Up to now, I only implemented the mechanics to place new rooms, without any fancy stuff. If a room can be placed at the cursor position, it is rendered in green and otherwise in red. No black magic here.

The room itself is nothing more than a placeholder. Later on there will be plenty of different rooms with different features, like a water recovery system, carbon dioxide reduction assembly and so on.

Next step
The next important step will be the implementation of simple people and the pathfinding for them.

Really small update: Rooms will now connect properly and the first citizens are there.

Rooms
Each room has multiple "Connectors". Each of those connectors is able to connect itself to an adjacent room. If it is connected, the floor will be rendered, otherwise a closed door is rendered. The connections are established whenever a new room is placed.

In the future, each of those doors will have a cost. This costs will be used in the pathfinding algorithm, to find the shortest and most efficient way from one point to another.

I think it might be cool, if the user can close those doors manually to seal them off in case of a leak in the hull. But that's not on my schedule now, maybe I will add it later on.


Citizens
I implemented really simple citizens with a basic walk animation. At the moment they are nothing more than a position vector, a direction and a texture. The next step will be the pathfinding algorithm, to let them walk around.

Now I am able to find a path between two rooms. This is done via the gdx-ai A* algorithm. Adding pathfinding support was really easy. I used the IndexedAStarPathFinder implementation from gdx-ai. Thanks to this, I only had to implement some little interfaces.

IndexedAStarPathFinder
First of all the IndexedAStarPathFinder needs an IndexedGraph. This graph consists of several IndexedNodes. Nodes are connected via Connections. The pathfinder will use the graph to find a path between any two nodes, if such a path exists. The pathfinder also needs a Heuristic. The purpose of the heuristic function is to return an estimate of the distance between two nodes. This is useful, because the pathfinder will check the nodes with a small estimated cost first.

IndexedNode
The graph will consist of several nodes. In my case each node will be a Room object. So I let the Room class implement the IndexedNode interface.

public interface IndexedNode<N extends IndexedNode<N>> {
public int getIndex ();
public Array<Connection<N>> getConnections ();
}

The index is easy, whenever I add a new room to the world, it will get the next free number, resulting in a consecutive order. getConnections must return an Array of Connections. To do so, my Connector class must implement the Connection interface.

public interface Connection<N> {
public float getCost ();
public N getFromNode ();
public N getToNode ();
}

The only thing missing in the Connector class is the getCost function. For simplicity I use the size of the room as the cost.

IndexedGraph
Now I have Connections and Nodes, the next missing piece is the Graph itself.

There exists a default implementation for IndexedGraphs, called DefaultIndexedGraph. This graph consists of an Array of nodes and will work perfectly, but there is a small thing that bothered me:

public Array<Connection<N>> getConnections (N fromNode) {
return nodes.get(fromNode.getIndex()).getConnections();
}

So what's the problem? Well, nodes is an Array and getIndex will return the index of that node inside the nodes array. So the whole function call will just return the fromNode we already have! Basically we could just write this:

public Array<Connection<N>> getConnections (N fromNode) {
return fromNode.getConnections();
}

In my case, I already have a GameWorld which has an array of rooms, so I let it implement the IndexedGraph interface and that's really easy:

public class GameWorld implements IndexedGraph<Room> {
// ...
@Override
public int getNodeCount() {
return rooms.size;
}
@Override
public Array<Connection<Room>> getConnections(Room fromNode) {
return fromNode.getConnections();
}
}

Heuristic
The last missing piece of the puzzle is the heuristic function. To make it really simple, I only implemented a heuristic using euclidean distance.

public class EuclideanHeuristic implements Heuristic<Room> {
public static final EuclideanHeuristic instance = new EuclideanHeuristic();
private EuclideanHeuristic() {}
@Override
public float estimate(Room node, Room endNode) {
return endNode.getCenter().sub(node.getCenter()).len();
}
}

The heuristic itself doesn't contain any attributes, because of that it wouldn't make much sense to have different instances of it, so I added a static singleton instance.

And that's all, now I have a working pathfinding algorithm. Thanks gdx-ai!

> Thanks gdx-ai!

You're welcome :)

Solar Colony is all about resources, you have to mine and produce resources, in order to survive. A citizen needs food, water and oxygen, so you have to produce those resources.

What resources?
The basic resources are some gases, liquids and solids. At the moment the most resources are gases. The most important ones are obviously hydrogen and oxygen. The oxygen is used by the citizens directly and also to produce water:
2H2 + O2 -> 2H2O

Every human will consume oxygen and will produce carbondioxide, so you have to get rid it. This could be done via the Carbon Dioxide Reduction Assembly (CReA). This unit will consume carbondioxide and hydrogen, in order to produce methane and water:
CO2 + 4H2 -> CH4 + 2H2O

If you like, you could now use a bit of that water to produce oxygen again...

But what could we do with the methane? Not much, you could either just discard it by blowing it out of the station or you could use it as a thruster. So basically both alternatives will blow it out, but the later one will do it to adjust the attitude. I'm not sure if I will implement that option, but I will keep it in mind.

Some code
Now a bit of code. The resources are nothing more than an enumeration. The first parameter is the short name, the second is the long name and the third parameter is the category of this resource.

public enum Resource {
// Gases
Hydrogen("Hβ‚‚", "Hydrogen", Category.Gas),
Deuterium("Β²H", "Deuterium", Category.Gas),
Tritium("Β³H", "Tritium", Category.Gas),
Helium4("⁴He", "Helium 4", Category.Gas),
Oxygen("Oβ‚‚", "Oxygen", Category.Gas),
CarbonDioxide("COβ‚‚", "Carbon Dioxide", Category.Gas),
Methane("CHβ‚„", "Methane", Category.Gas), // ... liquids, solids, etc. ... }

There are obviously three categories: gases, liquids and solids. Those are used for "normal" resources. But there are two additional categories: food and meta.

Citizens will have to consume food, but instead of having only one kind of food, there will be different kinds. If a citizen only consumes one kind, he will get sick. If he eat many different kinds of food, he will be healthy and happy, just like in real life.
So to simplify this, I added the "food" category.

The meta category is again a bit special. At the moment the only meta resource is energy. But maybe citizens will have a health resource or something like that.

Do not code when you're tired! A little story of bad design decisions. But let's start with the good part:

Storage
A space station cannot contain an unlimited amount of resources, so there has to be some kind of storage. This storage is basically just a map: Resource -> Amount

The two most important methods are

int get(Quantity resource, int amount)
and
int add(Quantity resource)

A Quantity is a simple tuple of a Resource and an amount. The add method will try to add as much items from the quantity as possible. The return value is the number of added items.

Let's look at an example:
We have an empty storage with a limit of 100 resources and we want to add a Quantity(Oxygen, 110).

There is not enough space for all of it, but at least a portion of it. So after the add call, the storage is filled with 100 "units" of oxygen and the method returns the value 100, because that's the amount of added units. The quantity itself is also modified. It now has a leftover of 10 units.

The get method is similar, but it has an additional parameter amount. This additional parameter says how many units we want to move from the storage to the given quantity. The return value is again the number of moved units. Let's continue with the above example. We now want to move 50 units of oxygen back to our quantity, which only contains 10 units at the moment.

After this call: get(quantity, 50)
Our quantity now contains 60 units, the storage has 50 units and the method itself returns the value 50.

And now the bad part...

Consume and Produce
I implemented a system to consume and produce resources. The basic idea is that a ProducerConsumer consumes an array of quantities to produce another array of quantities. For example: A Water assembler will consume one Hβ‚‚, one O and a bit energy to produce one Hβ‚‚O. So far so good, but where does a room get its resources from? From the global storage? From its own input buffer?

Late at night I came to the conclusion that I want both. Gases and Liquids will be taken from the station itself, by iterating over all storage units and removing the needed items. Solids will be removed from the local input storage. So every room has now an array of storage units marked as input, output or global. A storage unit marked as input will be only used by the surrounding room. Output will be filled by the surrounding room and every other room can use it's content. Global storage can be used for anything.

Really strange and ugly design, but that's how it goes when it's late and you're tired...

So I'm not sure how to go on. Should I fix this trash or should I keep it and hope for the best? I mean, it works, but maybe I will regret this design decision. But fixing this will cost time and time is a rare resource at the moment.

JSON serialization here I come! Now the GameWorld can be saved as a json file and also loaded from a file.

LibGDX json
LibGDX is able to read and write object graphs to and from json. This is done via reflection, but there are some cases, where the default serialization is not good enough. In my case there a world consists of different rooms and each room has different connections. When two rooms are connected to one another, each room will have a connection, with a reference to the other room. This circular dependency is a bit problematic, so I decided to do the serialization for some classes by myself.

So if a connection object is connected to another room, I just store the ID of that room inside the json object. When the connection is deserialized, I read the ID and store it until all rooms are loaded. Then I post iterate over all connections and fetch the target room from the GameWorld by its ID.

I also had a problem with (de)serializing an ObjectIntMap. After serializing and deserializing an ObjectIntMap, it's content was not the same! Maybe because the key was an enumeration, but that's just a guess. It also creates a lot of zeros, and in my implementation I skipped them:

json.writeArrayStart("keys");
for(ObjectIntMap.Entry<Resource> entry : this.resources)
if(entry.value > 0) json.writeValue(entry.key.ordinal());
json.writeArrayEnd();
json.writeArrayStart("values");
for(ObjectIntMap.Entry<Resource> entry : this.resources)
if(entry.value > 0) json.writeValue(entry.value);

This will result in something like this:

{ // some other stuff
  keys: [4]
  values: [51]
}


Not exactly json...
json is a nice format (and a million times better than XML), but there are also some not so nice things. For example in a normal json file the key of a map has to be a string, also the right handside has to be null, true, false, a number, a string, an array or an object. But obviously it would be much better to allow also simple identifiers on both sides. As you can see, standard json is a bit odd.

And now we bring LibGDX in:

It is able to produce not only standard json, but also the "improved" version of json! So the following code will be valid json in LibGDX:

{
  localPosition: { x: 32, y: 48 }
  direction: SOUTH
}

As you can see, we can also skip some commas!
The standard json would look like this:

{
  "localPosition": { "x": 32, "y": 48 },
  "direction": "SOUTH"
}
(Edited 3 times)

Update: added artificial intelligence to the citizens. "Intelligence" is maybe not exactly what I implemented, but they are now walking around and they can deliver resources from one room to another.

What's new?
Let's start with a little gif animation:

So the graphics are really ugly, but they are just placeholders.

The little guy you see there, was idling around at the Rock Crusher. Then the Rock Crusher broadcasts a message, because it has run out of Rocks. The little guy receives that message and is happy to help! So he responds and goes to the Meteor Collector, where he collects some rocks (or meteors). Then he goes back and drops them at the Rock Crusher.

Not a complex task, but let's dig a bit into code.

gdx ai
The AI of my citizens is based on statemachines of the gdx ai library. Each citizen now owns a StackStateMachine. To be exact: My own implementation of it. The gdx-ai StackStateMachine is nice, but I missed some things there:

  • A simple changeState method. The gdx-ai version will always put the new state onto the stack, but in my case, I often want to replace the current state with the new one. Instead of calling revertToPreviousState and then changeState, I added a simple popAndChangeState method.
  • The stack is private. In most cases that's really good, but I want to serialize the StackMachine. Sure, LibGDX can serialize it, but I don't want to serialize all States. There are some states I don't care about. The GotoState is one of them. It contains a GraphPath and the start and end room. But instead of serializing it, I discard it, because the parent state is able to reproduce it anyway.

Most states are implemented as a simple Java enumeration:

public enum AI implements State<Citizen> {
  Idle() {
    public boolean onMessage(Citizen entity, Telegram telegram) {
      // handle messages and maybe switch the stateMachine
      // ...
      return false;
    }
  },
  GotoSource() {
    public void update(Citizen entity) {
      // goto to specific room and grab resources
      // ...
      // this will add a GotoState if we are not at the source:
      // if( not near source ) gotoRoom(entity, job.from);
    }
  },
  DeliverGoods() {
    public void update(Citizen entity) {
      // goto to target room (add GotoState if necessary)
      // and drop items...
    }
  };
  public void gotoRoom(Citizen entity, Room target) {
    GameWorld world = entity.getWorld();
    BasicGraphPath path = world.findPath(world.getRoom(entity.position), target);
    if(path != null) entity.stateMachine.changeState(new GotoState(path));
  }
}

Of course, because the behaviour is described by an enumeration, I have to store any additional information inside the citizen. In this cases it is ok, because the only thing I need is a MoveJob instance. For other states, like the GotoState, I have to store a bit more data, like a GraphPath, intermediate target, speed, etc. So instead of putting that into the citizen, I added an extra State, which is not an enumeration, so it can store its own data.

A lot new stuff since the last blog entry. First of all, Solar Colony now looks a bit better, because of some new graphics. But I also changed a lot of internal stuff.

Startmenu
One change is the startmenu. Now one does not immediately start in the game, but inside a start menu. There you could create a new game or load a savegame.

The nice starfield is also the background of the game itself.

The three different difficulties will result in a different starting station. The game itself is also a bit easier or harder. On hard, each citizen will consume much more oxygen, food and water.

On easy, a demolished room will give you its full cost back. On hard you will only get half of it back.

UI cleanup and room removal
The UI was a real mess. Now it is a bit better. I added a Scene2D table on the left side and added all buttons to it. Each button now has the same size and they are also a bit bigger now.

One new feature there is the destroy button. Up to now it was not possible to remove a room from the station. Now you can destroy rooms by activating the destroy button and clicking on rooms. But you can only destroy a room if there aren't any people in. If a room is removed. All its content is moved to other storage, and depending on your difficulty, you will get some of its cost back.

I also added some icons. If a citizen is hungry, thirsty, etc. a corresponding icon is shown. If a storage is full or a system is not able to produce something it is also shown by an icon.

Graphics
I have added some new graphics, but as you can see, there are still a lot of graphics missing. The character sprites are ment to be placeholders, but I think I will keep them, because they are not as bad as I thought.

As you can see on the gif animation, I also added moving meteors. They basically just some nice looking feature, without any gameplay mechanic. They are moving from right to left and will be deleted if they are near the Meteor Collector facility. But the collector itself will always produce the same amount of meteors, independent of the colliding meteors.

Cloning
Originally my plan was to let the people reproduce themself by the normal way. But that would involve more AI, rooms for the people, children and so on. But because of a lack of time, I wasn't able to implement all that stuff. So instead, I implemented a cloning facility. To generate new humans, you have to install a single cloning facility. You can then use the "Place Character" button to generate a new human, but only if you have enough water, oxygen and biomass.

Gameplay balancing
I started balancing the game itself and yes, it is really hard to do so. I added more build costs to the rooms and adjusted some converters. At first my people died very early and I had to make more adjustments. Now it looks a bit better, but it is far away from being balanced. I will try to find some time to adjust it, but we will see.

Finally on itch.io
Solar Colony is done! OK, it's more a prototype than a real game, but you can play it anyway :)

The (ugly) source code is now available on GitLab.

Getting started
First of all, every space station needs storage for gases, liquids, solids and energy. If a storage is full, your machines stop producing! So make sure that you have always enough space.

Most buildings will cost metal and electronics, but some will need a bit more, like plastic. To get metal, you need a Meteor Collector, which will produce rocks. The Rock Crusher can use those rocks to produce metal and silicon.

To generate electronics, you need a Circuit Factory. It will then generate electronics out of silicon and plastic. But how do we get plastic? Plastic is generated by the PlasticGen, which will in turn need again other things...

So as you can see, you have a lot to do. At the beginning it might happen that your people will die pretty fast, so I guess, you will have to reload a lot of savegames ;)