Here we go again... (porting Cthangband, to Godot this time!)

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • Dean Anderson
    Adept
    • Nov 2009
    • 194

    Here we go again... (porting Cthangband, to Godot this time!)

    Some of you may remember that a few years ago (2018, so that's eight years ago now) I ported Cthangband from C to C#.

    While this allowed me to remove decades of cruft from the code and make it a much cleaner object-oriented codebase, it had a couple of disadvantages. Firstly, I was compiling it as a WPF application which restricted it to Windows only, and secondly I was relying on the serialization capabilities of the .NET Framework 4.5.

    Time makes fools of us all, and now both of those have come back to bite me.

    Around a year ago, I switched from Windows to Linux due to Microsoft's increasing enshittification of Windows 11, Unfortunately this means that without breaking out a virtual machine I can't even run Cthangband any more, let alone compile it. And if I did want to compile a new version, I'm stuck with .NET 8 which doesn't have the old serialization functionality (it was removed for security reasons).

    So, like the masochist I am, I'm porting it again...

    I've been using Godot game engine for the last year, making a conversion of Ularn as a practice project, and I'm now reasonably confident with the platform. It has the advantage that it can build Linux, Mac, Windows, and HTML 5 Web versions all natively so it means I get cross-platform compatibility back, and it's also FOSS, which is a bonus.

    It also has the advantage that I will be able to put the source for the Godot project online and people will be able to dowload/compile it themselves rather than needing to trust the download of a precompiled executable or installation script.

    The big disadvantage is that Godot's native language - GDScript - is Python-based rather than C-based. So once again I'm having to port every line of code (a quick check tells me there are around 125,000 lines of code in Cthangband) from one language to another with a different syntax.

    Except that this time I'm also having to switch from procedural code that occasionally idles waiting for a keypess to a fully event-driven model that runs in a main render loop and fires out events (or "signals" in Godot-speak) when keypresses and other input happens and which expects most of your code to run between frames.

    I've not got very far yet, but the basic structure and user-interface is there and I've got as far as handling the main menu, loading/creating a new game, and going through the character creation screens. It's surprising how much I've needed to implement to get that far, because even simple chargen things need a lot of data and other systems to be set up.

    For example, to be able to display the character sheet at the end of the chargen process I need to call the function that updates your character when a change has happened (in this case, the change being that the character is newly created). This function takes into account what your character's ancestry and profession are, which is fine because the basics of those needed to be implemented anyway for you to be able to select them), but also what effect mutations have on your abilities, so I had to implement the mutation system, and what effect your items have on your abilities, so I had to implement parts of the item system, and when your character's birthday is and whether they start at night or during the day, so I had to implement the game's timekeeping system.

    So in terms of implementation, I'm taking a depth-first approach, slowly moving forwards in terms of progression into the game while diving deep into the subsystems required as I take each step forwards.

    It's going to be a long haul, though.
  • Nick
    Vanilla maintainer
    • Apr 2007
    • 9398

    #2
    You're braver than me
    One for the Dark Lord on his dark throne
    In the Land of Mordor where the Shadows lie.

    Comment

    • Dean Anderson
      Adept
      • Nov 2009
      • 194

      #3
      Well, I've converted the first couple of thousand lines of code so far, and it's not going too badly.

      When it comes to a port of this size, what to do first is a very interesting question. Do you start with some kind of Minimum Viable Product where you have a character walking around and then start adding features? Or do you take a more monolithic approach and start at the "beginning" of the code that runs when you start the program and go from there?

      The former gets you quicker visible results, and is almost certainly better if you're writing a game from scratch, but it can have the problem that it's less methodical and you end up missing things in a port, so I've gone for the latter approach.

      I obviously started with the "Get an 80x45 grid of ascii characters to display and update, with print functions" basics, but after that I've gone for starting at the beginning.

      I mentioned in the first post that I'd got as far as character creation. It now does post-character-creation initialisation too, loading all the template data for monster type and item types and so forth, initialising the flavours for potions and scrolls and things, and creating the wilderness map, towns, and dungeons.

      Next on my list is creating the town level, and then we'll eventually have got to the point where we can do the basic turn structure and display code to let you walk around...

      From a language point of view, it's not too bad. GDScript is a proper OOP language with classes and inheritance, so I need to do a lot less restructuring than I did when moving from C to C# where I had to convert from procedural code to class-based code. I can mostly simply port functions one by one.

      Sure, the syntax is different, going from a C-based language to a Python-based language, but for the most part (at least so far) that's something that requires small-scale conversions on a line by line basis. I can keep the same classes with the same methods and properties and just convert the individual implementations.

      The main sticking points are that GDScript doesn't have interfaces and the way input is handled.

      Input is a particularly sticky issue.

      In the WPF version you had an "Application.DoEvents()" command which would tell the application to handle all pending events from the OS. This includes things like the window moving or being minimised, but also keypresses. This meant that I could have a simple "GetKey" fumction which was basically an infinite loop that checks the key buffer for a key with a DoEvents inside it. The loop starts running, and if the user presses a key it an event is fired which adds the press to the key buffer. The function then detects this and breaks out of the loop returning the key.

      Unfortunately for me, there's nothing similar in Godot. Instead it follows the standard game-engine paradigm of having a render loop and a process loop. There's no simple way to pause mid-code in a function and wait for a keypress before continuing, because while you're waiting the engine isn't processing events. Instead you have to do one of two things.

      The first is to make a state machine where instead of a single process the game runs in little single-input chunks and then each chunk finishes and it goes back to idling before a keypress happens and it runs again to process that input. That's fine for some things, but it falls apart when you have to do things like ask the user for something in the middle of a complex command. There's just too much state information to persist between inputs.

      The second is to use the "yield" paradigm for co-routines (although Godot implements this with an "await" command). So you can have your process running code and then use "await" to wait for a specific event such as a keypress event before carrying on. This sounds like what the WPF was doing but the important difference is that it doesn't pause the process/thread. Instead it drops out of the function and continues processing, returning to complete the function later when it gets the event.

      Naturally, this isn't what we want, because we want to stop and wait for a keypress rather than continue on with whatever function called the GetKey function. You can get around it by having the outer function await the completion of the GetKey function, but this soon results in proliferation of awaits and ties the whole game up in a mess.

      So I'm using a hybrid of the two. I've got a signal bus that sits there receiving keyboard inputs and then has a bunch of events that it can fire. It checks the state we are in and then fires the relevant event, and the various game components (menu, load-screen, game-screen) all catch only their specific variety of event. This means that most of them can idle between function calls and just wake up when the game is in the correct state.

      However, for the more minor places where input needs waiting for, like asking for quantities or targets mid-command or something, I use await.

      This means that I don't need special stats for the minor places and it means I don't need to proliferate the awaits all the way through the code, only down to the "process the player's in-game command" function.

      Comment

      Working...
      😀
      😂
      🥰
      😘
      🤢
      😎
      😞
      😡
      👍
      👎