Why multiple layers?
Multiple layers are basically nonexistent in terminal emulators. For 99% of cases there is simply no need for them. One character for one cell. Period.
Still, are they completely useless?
Personally I have found a few cases where they could be useful.
For example, utilising the alpha channels we can do things like GUI that overlays actual game with a dark semi-transparent background.
Another example is presenting more information at once. Let’s say we want to implement some beautiful rain in our game by rendering blue ‘/’ characters every now and there. In case of a single character per cell, the rain would need to overdraw whatever was behind it in some way, as we cannot draw two characters in same place.
For big objects that consist of multiple characters, this may be not an issue. Seeing a rain drop over one cell of a big building won’t make us stop seeing the big building.
But what happens if a rain drop would be rendered over an enemy? Should we not render rain drops over some objects? Should the enemy disappear from player’s vision every now and then because of some rain?
This might be confusing to a player so rendering a character on top of another one might be a better choice.
There are obviously more cases where the layers could prove to be useful, especially in games but these are just some that I came up with during experimenting and developing Praedium.
While multiple layers could be implemented as easily as two or more terminals stacked on top of each other, this naive approach would definitely not be the most optimal or convenient.
Firstly, for each new layer we would introduce another terminal. This may not seem to be represented by a big data structure, and it presumably would be quite sparse in most cases, but we still would have to hustle with tracking the terminal contents every frame and looping through every cell in order to render it (if it’s not an empty character of course).
Secondly, there are many cases where terminal-based approach, even if the rendered results really does look like a terminal-emulated environment may not be a good choice.
If we are working inside an environment where camera moves a lot and introduces a lot of change on the screen, if our screen is represented internally by some kind of terminal - we have to do a lot of value changing inside it in order to update what is present on the screen. This may be a non issue when the game is turn based, as we would only need to change terminal contents (and redraw) only on player input. But in case of games that introduce real-time changes and the terminal contents need to be updated every 30 or 60 times a second to keep up with the frame rate, this very quickly starts to pile up and become a problem.
Because of these issues, I had to change my initial assumptions about whole rendering layer.
Initial concept and how it changed
Initially my concept was simple - what is rendered on screen is represented by a
Malison.Core.Terminal instance that gets updated on player input just like a typical rougelike game.
Then, we loop through every renderer (a rendering component of some game object) and it receives some kind of terminal as an argument passed to it’s
Now, let me explain what exactly my reasoning was to pass some terminals to renderers.
The Malison terminal emulation library introduces some interesting concepts and one of those was the clever usage of indexers.
Let me explain by a simple example - writing a ‘Hello World’ with given colors on given position inside some
Now, if you got confused I’ll make it simple. The first indexer call the
[2, 4] was accessing the second row and fourth column of the terminal as our cursor’s starting position.
The second indexer call - the
[TermColor.Red, TermColor.Yellow] sets the foreground of the cursor to red and background to yellow.
After that, a simple
Write() method call is present.
While that may look like just some fancy API, beneath it a few interesting things happen.
Firstly, whenever we call any indexer on given terminal, we get a new and modified terminal instance.
So, the first call creates a new terminal, with cursor set to
[2, 4], the second one creates another terminal that also has changed cursor colors.
Finally, the Write method call sets the data inside the original terminal instance.
If you are asking yourself why would anyone go through so much trouble then let me just say - it’s easier that way.
Why? Because tracking and changing/reverting cursor position or colors is very annoying. This completely removes the issue of forgetting to change/reset cursor colors or cursor’s position in order to render everything properly.
Also, other than accessing the terminal by colors or positions, we can access a subset of the original terminal through these fancy indexers.
That way we can only work on parts of the terminal and forget about writing characters outside of it’s bounds!
Want a part of the screen to be used as a GUI only? Subset a rectangle of the original terminal and now you have access to it as some different, isolated view. Isn’t that nice?
That’s why I decided to implement the rendering layer that way, so I could pass some partial terminals to renderers, and those would not know the difference and act nicely.
Still this approach had to be changed. Performance started to be an issue, needs for multiple layers arose. Renders had to do less and not be bound to terminal instances.
I started using the SFML’s primitives and treating most of the game objects as simple sprites rendered on screen. No need to update some terminal contents each frame just because of some camera movement.
Now, I’m not abandoning the terminals completely. There will be many places where those will be very useful, such as GUI or easy text rendering.
For these cases I have created a new type of renderer that simply renders the contents of some terminal, whose contents are changed by the game object the renderer is attached to.
While this is yet another change inside the Praedium’s internals it was definitely not really a big one. I feel like I did more thinking than writing code behind this one, as it implied a lot of changes for the rendering layer, and the rendering flow of any things to come.
On one hand I feel kind of bad that so many things I initially set up had to change but on the other it results in faster and more robust code.
The rendering problems disappeared, the engine code is cleaner, SFML allows me to go multi-platform and engine itself is shaping up quite well.
Lots of these changes were related to design choices but still ended up on me expanding the engine or my terminal emulation library. It’s quite an interesting experience to simultaneously work on your game, the engine and dependent libraries at once.
That’s it for today’s post! If you have any questions or feedback, feel free to drop a comment below.
See you next time!