From Ruby/Gosu to Godot, aka Godosu
As I noted in the game's description, Son of Sword was originally written in Ruby using Gosu library. It's a small lib that I found by chance when I just started learning how to program. This post describes how I eventually ported it to Godot Engine, as well as revealing some behind the scene stuff about that and my other games.
If you are interested only in the porting part, skip to Initial Porting Attempts section.
Origins
Son of Sword was made around 2011, when I was a programming beginner. In around 2008 I got irritated with RPG Maker, and after I learned that Ruby which it uses for scripting is actually a real programming language, I ditched that engine to learn programming and actually code my games. Around that time I also found Gosu and became one of its heaviest users. In total I created 9 finished, or at least advanced, projects and Son of Sword was 3rd of them.
As almost all of my games at the time, Son of Sword didn't get popular. It was one of the more ambitious projects, because I even made a trailer, but I didn't really do any marketing, aside from 2 YouTube videos and a thread on Gosu forums. The game was always going to be freeware, but I didn't really think too much about sharing it. At that time I was mostly making games for the fun of it tbh (though that didn't change much, even if I'm nearing my first commercial release). The projects I made were still crafted with love, and Son of Sword is especially polished one. All art was made by me and the game is surprisingly well-scoped, something I usually struggle with in my projects 😅
Godot
I started using Godot for real sometime in 2019. My adventure with Ruby/Gosu was very long, but eventually I felt like it's not really a way forward. I think I used every possible feature in that library, and a few of its extension libraries, in many ways that no other person used them. Gosu is great for 2D and I really liked using it, but its biggest problem was portability. The games I made were running perfectly fine... on my PC. Over years I encountered many compatibility problems that I had no way to fix. Another thing is that there isn't really a way to make a Ruby executable, other than wrapping the interpreter into a self-extracting executable and basically distributing the game's source.
This led my to try many different game engines over the years. XNA, Unity, Torque 3D, Panda3D, Neo Axis... This was also when I was doing some experiments with 3D projects (to this day I never created a real 3D game) and after every try I just went back to using Gosu. Other frameworks/engines felt like I had to fight them to get something done the way I want. My beginnings with Godot weren't well too tbh. It was one of the engines I bumped from, until 3.0 was released and I gave it a try again. I eventually grew to like it, because from all the engines I tried, this one I had to fight the least. I eventually learned Godot well enough that it became my main gamedev tool and using it doesn't feel like a struggle.
The idea about porting my Gosu games to Godot appeared some years ago. I thought that it would be a waste if my games remained in obscurity forever. They are playable and arguably fun. Wary of the various problems I had with Gosu, and with Godot becoming my main game engine, I thought maybe I could make my games run inside Godot. The idea was not unfounded, because I got aware of how Godot's source access gives limitless possibilities. Especially as I somehow became one of the engine's top contributors 🙃
Initial Porting Attempts
Godot supported potentially any third-party language via GDNative, so I though that maybe it could run Ruby. It couldn't. There is some Ruby GDNative port, but it's not really usable. So instead I looked into maybe embedding Ruby in Godot and so I found MRuby. It's very small and integrating it was very easy, but it turned out to be super limited. Like, you can't even define a module in MRuby. My idea about re-implementing Gosu on top of Godot was impossible. Another thing I tried was Crystal language. It's supposedly similar to Ruby and there was a chance my games would be compatible. However I failed to integrate it with Godot; tried it with the new GDExtension, only to find out that Crystal is not intended to be used as dynamic library.
Embedding Ruby
The idea of porting my games kept haunting me though, so from time to time I did some random search for the solution for my problem. And I found this article: https://silverhammermba.github.io/emberb/c/ It's great, because it gives all the information you need to start hacking with Ruby interpreter; I'm using it as reference to this day.
That's only one piece of the puzzle though. I can't use Ruby C API if I can't embed Ruby. I tried to make a custom Godot module that uses Ruby as dependency. I spent many hours researching and many failed attempts to come up with the final setup. Though the main problem is that I'm developing on Windows.
Integrating Ruby with Godot involves setting up the scons config file. My first successful attempt was on Linux. It has Ruby installed by default, so all I had to do is add ruby lib to dependencies and it just compiled. Developing on Linux is usually a breeze, because compiling big projects is just running one command most of the time. However I only ever used Linux via VMs and developing on a VM is annoying so I obviously wanted to get this running on Windows. So far Godot is the only big C/C++ project I tried compiling where Windows build support is not complete ass. You just run one command, no configuration and it works. That's not the case with other projects I tried building, and obviously wasn't the case with Ruby, where the Windows support is obscure and you need to go through some hoops.
I originally tried using the development DLLs shipped with Ruby installation, but the engine failed to build using that. I eventually managed to compile Ruby myself using this guide: http://software.firstworks.com/2016/01/building-ruby-from-source-with.html. What it doesn't mention though is that you need to pass --target=x64-mswin64 for configuration (because the build defaults to 32 bit 🤦♂️), and in my case I also used --disable-install-doc, because it only caused additional problems. So the configuration part goes like .\win32\configure.bat --target=x64-mswin64 --disable-install-doc. That still wasn't enough though, because I had to download some win_flex_bison lib and add it to my PATH. Eventually I managed to compile Ruby and then after another long session of fighting with scons I managed to integrate it with Godot source as a custom module - Godosu.
Godosu
Godosu is a custom module that re-implements Gosu inside Godot. The code is available publicly in my Godot fork: https://github.com/KoBeWi/godot/tree/godosu/modules/godosu. Here's some brief summary and highlight of more interesting parts.
Godosu module consists of 2 parts: the C++ module part and Ruby part. The C++ part is responsible for registering methods and providing interface for rendering, input etc. It provides a Godosu node, which you put in your scene and setup with the main .rb file. The Ruby part is a middleman between game's code and the C++ methods. It defines Gosu module with the same API as the original, but instead calls Godot methods.
Getting Gosu to work inside Godot was tricky. The base of a Gosu game is Window class, which provides virtual methods called update() and draw(). Update is intended for game logic, while draw is for drawing. The thing is that, unlike Godot, Gosu has no "scene" structure. You are given the bare Windows and have to implement all systems yourself. Drawing is done using Image class, which is an instance of a loaded texture. This is similar to Godot, except the Image draws itself, while in Godot you need a CanvasItem node to draw something. Also each Gosu's draw command includes z-index. The way I solved this is that Godosu node holds a HashMap of CanvasItem nodes, with composite key that includes z-index. When a draw command is sent from Ruby, Godot will find a matching CanvasItem and put the command in the queue. Then at the end of process frame it will request redraw of all CanvasItems, which will then process their assigned commands in draw notification.
Draw commands themselves are more or less straightforward, except the draw_rot() method. It's a Gosu method that allows drawing an image with rotation, center pivot and scale. Currently the only way to draw a rotated texture in Godot is doing some transform voodoo, which unfortunately did not work that well with scale. I ended up copying the Gosu implementation, you can see the code here. It ended up being the most complex command, also thanks to a workaround for draw_polygon() not supporting AtlasTexture. I use AtlasTexture for Gosu's load_tiles() method, which splits an image into an array of sub-images.
The way I handled images is also somewhat interesting. In Gosu, Image is created by loading an image file. This is the same as in Godot, but the problem is that I need the image loaded on the C++ side using Ruby methods. My solution is that whenever an instance of Image (i.e. the Ruby-side class) is created, it requests Godot to load that image and the Ruby class instance is used as a key in image cache HashMap. Whenever draw() command is invoked in Image, the Image will request Godot to draw the texture associated with its instance. I did the same for Gosu's Song and Sample classes (which both map to Godot's AudioStream). Song is played using a single music AudioStreamPlayer, while sounds each create their own AudioStreamPlayer that plays them and disappears.
Advanced Godosu
Some features required more tricks to implement them. For example, Gosu has a translate method, which offsets every draw command in in the passed block (Ruby's blocks are similar to lambdas). It's useful for moving cameras. The way I implemented this method is adding global variables for x and y translation. My translate method will set the global variables to desired value, execute the draw block and reset the variables. When draw command is passed to Godot, its position will be offsetted by the global value. Another slightly problematic thing was z-index shenanigans. Gosu supports floating point z-index and also provides flush() method, which makes every subsequent drawn object appear on top of everything before flush() was called (similar to Godot's CanvasLayer). The way I solved it is that z-index is multiplied by 100 and flush() adds a base z-index 1000 to all further draw commands. It's simple and works well enough, but limits z-index precision to 2 decimals and goes dangerously close to Godot's 4096 z-index limit. Fortunately both limitations are fine for my games.
More difficult features include clipping and text input. Clipping, like translate, takes a block of drawing code and clips it to a specified rectangle on screen. Godot only suports clipping by using a parent CanvasItem that defines clipping area, so I'm using the clipping rectangle as part of the key for the CanvasItem HashMap. When clipping is requested, the target CanvasItem will have a Control parent that clips it to the desired area. This is when I had to do some optimizations, because in Son of Sword I have an object that uses a very dynamic clipping rectangle, which resulted in CanvasItem+Control spawned every frame. One of my optimizations is reusing clipping rectangles of the same size (the new request will just move the rectangle to new position). Another one is freeing CanvasItems that didn't process any draw command for the last 5 seconds. While spamming unique draw groups can still create unreasonable number of CanvasItems in your scene, they will be at least freed after some time, so your memory usage does not skyrocket forever.
The TextInput feature is interesting, because Gosu provides a very simple interface for it. Basically you have a base TextInput class and you set Window.text_input to a TextInput instance. Then the Window will just eat all your input and instead insert text in TextInput (which you can access using its text property). The user is responsible for displaying the text and caret. Here my solution was that each TextInput instance has an associated LineEdit node. When Window gets TextInput focus, its linked LineEdit grabs focus and takes user input. This class couldn't be implemented in a simple way, so I had to forward every property access to the LineEdit. Also, since Ruby is garbage collected, the TextInput can be gone at any time once references are dropped and luckily Ruby provides a mechanism to run a callback when Object is destroyed - define_finalizer. It's a bit tricky, but I use it do free the LineEdits once they are no longer needed.
The final boss of advanced features is Ashton. It's a separate library that extends Gosu with new functionality. For Son of Sword I needed shaders, but it was surprisingly easy to implement, despite the shader workflow is completely different from Godot. In Godot shader is part of CanvasItem, while in Gosu/Ashton you can specify shader per draw command or for a given z-index. The reason why it was easy is rather obvious - I already spread draw calls between multiple CanvasItems, so I just added the shader as another key part for CanvasItem HashMap. When drawing with shader is requested, the target CanvasItem will have the shader applied as a material. Simple as that.
Porting Son of Sword
Running Son of Sword in Godosu was super easy. Of course the development of the module happened in parallel. My workflow was basically start the game -> see what doesn't work -> implement it. I implemented the Gosu features to be faithful to their originals, so the game runs the same code and works the same as in "native" Ruby/Gosu. The code is not 100% untouched, because I used newer Ruby version and had to upgrade some methods. Also the aforementioned shaders were written in raw GLSL, so to use them in Godot I had to rewrite them in Godot's shading language (fortunately they are simple and there isn't many of them).
I did a full playthrough of the game to make sure that everything works correctly. In the process I also found some bugs in the game itself. I left most of them as is, but one was game breaking, so I had to fix it. Delving into the code I've written over 10 years ago was interesting experience. At that time I wrote code that's supposed to work and be short, disregarding any good practices and sanity. For example this is code for drawing player:
if attack frame=[11-@attack,10].min swordpoint=[[3,14],[6,12],[9,9],[12,8],[18,9],[21,11],[23,14],[26,18],[27,22],[26,26],[24,30]][frame] swordpoint[0]=32-swordpoint[0] if @dir==:left img=Tls['Player/Stand Attack',[32,63]][frame] else frame=((@vx.to_i != 0 or @water && @vy<0) ? [0,1,2,1,0,3,4,3][($count/4)%8] : 0) swordpoint=[[18,36],[20,34],[21,32],[16,37],[11,38]][frame] swordpoint[0]=32-swordpoint[0] if @dir==:left Tls['Player/Walk',[32,63]][frame] end.draw(@x+(@dir==:left ? (attack ? 35 : 32)*scale : (attack ? -3 : 0)),@y+1,1.5,scale*(@dir==:left ? -1 : 1 ),scale,c)
Ruby syntax is extremely flexible, and I did not hold back from abusing it in every way. In the code above, there is if block that runs some code for determining the base texture. Then directly on the block's end I'm calling draw() method, which draws whatever the last expression executed in the block. The game's source is full of such monstrosities. I can still understand it, and to be honest, I somewhat miss that way of coding. Back then, everything was simpler ;) While it looks horrible, it was much faster to write, even if I had no autocompletion when creating it. The game's source code is available with the executable, so you can look inside if you are brave enough.
One major bug I found is that the count of power balls (i.e. collectibles to level up character stats) was off. To max out your character, you have to collect 180 balls of each of the 4 colors and one color was missing 3 balls, while 2 colors had 1 extra. I was able to again use the extensive tracking tools I made for the game. I had a small tool that scanned all game's maps to display the ball ids you can find them (each of 720 balls has unique ID). I also had a txt file that listed all IDs and where to find them. Unfortunately it was not 100% accurate, so I had to launch the editor map editor and hack some fix. Of course Gosu does not provide any sort of editor, so everything was made by me. The editor itself is very obscure, and while it was usable when I was making the game, now its operations are a mystery (especially when Godot's excellent editor spoiled me). The feature I needed did not even exist, so I hacked in some code to fix the bug in my maps. Everything is still working after all these years.
Future
As I mentioned in the beginning, I made over 9 projects in Gosu. You can find the list here: https://www.reddit.com/r/gosu/comments/8x2spf/some_of_my_gosu_games/ I plan to port most of them. I started with Son of Sword, because it has the best completion to complexity ratio. Out of these games the Bouncy Square is the easiest to port, because it only uses Gosu, while Bottomless Mine will be the most difficult, because it uses every possible Ashton feature and features raw OpenGL code (which I don't even know how should I port).
Godosu itself is not complete, but I also don't really plan to make it complete. The whole purpose was to run my games, so I will probably skip some features I did not use (though I used so many of them that it will be just some single methods/properties, if anything). One big feature used by most of my games are Ashton's framebuffers, which are going to be the next complex thing I will hack together. Doing that will be interesting.
Congratulations if you made it to the end of this post ;) While it was long, it still probably missed some details. As I mentioned, Godosu is publicly available, but the code was written to run on my machine. If you plan to make a custom build, be sure to modify the scons configuration.
Get Son of Sword
Son of Sword
A mix of Eternal Daughter and Golden Sun.
Status | Released |
Author | KoBeWi |
Genre | Platformer |
Tags | 2D, Metroidvania |
Languages | English |
Leave a comment
Log in with itch.io to leave a comment.