Logo 3.5Cats_AndHalfAFish

3.5Cats_AndHalfAFish

IA.

February 25, 2020 Shade, Godot, 3d, interactables
Aaargh !

Note : this code was written in Godot 3.2.
 

Let me start by saying that I have rewritten this post several times, and that the end result is nothing like what I imagined it was going to be.
Yes, in figuring out this particular bit of workflow I have definitely taken the scenic route ...

When I was working on my first two point&click games (not made in Godot), I ended up creating 3 types of objects : 'interactables' (IA), 'pickups' (PU), and 'hotspots' (HS).

Hotspots are the 'things' that make interactables and pickups work. They are small areas (rectangles, in a 2d game) that can be made to respond to mouse clicks, or that can be used to 'receive' other game items.

Pickups are game items that have HS's that can respond to mouse clicks. Clicking on a PU moves the game item from the game world into the inventory. It can then be used on other objects in the game world or in the inventory.

Interactables are usually a bit more complex to write, because they can have both types of HS.
Think about a locked door. The door needs a key (receive) → the key needs to be turned to unlock the door (click) → and finally, the door can be opened (click).
Often, interacting with IA's also involves playing animations. In BetterWorld, the dragonfly is an IA that responds to repeated mouse clicks with a different animation.


The plan with this new game was to use a similar setup. But ... Aaargh ! Class-based code (Java, Haxe, ...) just does not translate well to the node/script system of Godot ...

The big picture.

As this game is intended to be a bigger game than the last one, I wanted to make sure that it was going to be easy to set up IA's. I also wanted to be able to reuse 3d models and scripts.
This is the workflow I settled on :

  • create a 3d model in Blender
  • create a collision mesh for the 3d model in Blender (simplified geometry ; name : '...-colonly')
  • import the 3d model (and collision mesh) into Godot as a scene (not : MeshInstance)
  • open the scene and add hotspot nodes for all possible interactions
  • create a new 3d scene with an IA node as root ; add as many of the previously created 3d model scenes as needed.
  • make the IA root node children 'editable'.


This is a silly example of an IA with 3 keys :

IA scenetree.
IA and 3d models.

'SB' is the StaticBody/CollisionShape that was generated in Blender ; it is used to prevent the player from walking through a solid object.
'HS' is a custom Area/CollisionShape node that can handle mouse clicks.
'MI' is a 'MeshInstance' ; it represents the visible part of the model.

A new material. Note that key1 (yellow) and key2 (light brown) are actually the same 3d model !
Key2 was given a different color by dragging an existing material into the material slot of the 'key' MeshInstance INSIDE THE IA SCENE.
Remember that if you change the material inside the 3d key model scene, all keys in all IA scenes will change !


So far, so good. But now consider this IA :

IA : chest with key.
IA : chest with key.

Scenetree : chest with key. We have a chest model and a key model combined into one IA.
At the start of the game, the chest is locked. It is waiting for the yellow key (receive). The yellow key needs to be turned (click) to unlock the chest. Once the chest is unlocked, we can click the lid to open the chest.

For each state of the IA, we have to update the relevant HS's.
For instance, at the start of the game, the only active HS is the one that can receive the key. Once the key has been received, that HS has to be disabled, and the HS for the key has to be enabled. Etc.

We also need a bunch of StaticBodies to prevent the player from walking through the chest and key. When the key is invisible, we should make sure that its StaticBody node is disabled as well.

And this is where it gets really complicated : we also need to write those state changes to our save_game file !
If the player inserts the yellow key into the chest lock, and then saves the game and quits ... When he next starts playing the game again, he will be expecting to find the key still in the chest lock !

Detour.

In my earlier attempts to store all this information in my db_state.json file, I had to write something like this for each IA in the game :

JSON file : IA state info.
JSON file : IA state info.

Yuk.
However, I wrote a short function that I could run whenever I had finished creating an IA, that would create that information for me. I only needed to copy/paste it into the JSON file ... and done.

But, it quickly turned out to be a pain to use something like this. It's error prone, and there's a bunch of code required to inactivate/activate HS's and StaticBodies whenever a model becomes (in)visible, or when an IA 'receives' the correct pickup.

The one good thing that came out of this ... mess ... is that I now understand why games do this (annoying thing) :


One less state to save ! =)
In case you were wondering ... this is from Riven (probably the best point&click game of all times).

Long live 'simple'.

I don't recall how I stumbled upon this solution, but it seems to be exactly what I had been looking for.
It requires that every IA has exactly 1 AnimationPlayer.
The AnimationPlayer has 1 Animation for each 'state' of the IA ; one of the Animations has to be named 'default' and it contains the initial state of the IA at the start of the game. 'Changing state' now equals 'changing animation'.
And that's it.
No more code required !

AnimationPlayer with 'state' Animations.

Of course, things could be further improved upon because ... well, it quickly becomes tedious to create an animation with tracks for all MeshInstances (vis/invis), StaticBodies (disabled or not), and HS's (disabled or not) in the IA.
So, I resorted to my previous trick and created a small function that can create the 'invis' and 'default' Animations for me, whenever I need them. The 'invis' Animation is created with MeshInstances invisible and SB's and HS's disabled. The 'default' Animation is the reverse.

Code : creating Animations.

Afterwards, the Animations can easily be altered if necessary. For instance, for the chest with key IA, the default Animation should have the key invisible (and its HS and SB disabled).

Adding more Animations is very simple : select an existing Animation, click the 'Animation' button, select 'duplicate', rename. Adjust tracks as required.

If you want to reorder the list of tracks with drag and drop, you first need to press the 'list' btn below the tracks :

Reorder animation list.
Reorder animation list.

Saving IA 'state' has now become ridiculously simple ; I only need to store the name of the Animation and the position the Animation needs to start playing from (seconds) :

JSON file : IA state info (v2).
JSON file : IA state info / AnimationPlayer.

To actually set this animation, I use :

Setting animation in code.


As an aside, I've noticed that the AnimationPlayer node seems to behave in a different way than other nodes and resources.
It would make things easier if I could for instance create an AnimationPlayer for one type of chest, and then reuse it slightly modified with another type of chest ... But, unfortunately :

  • Scenario 1 : select the AnimationPlayer in the scenetree and click 'save branch as a scene'. Import that AnimationPlayer scene into another IA scene and change the tracks a bit (eg. make a mesh invis). Save the IA scene.
    Result : the Animations have also changed in the original AI scene ... Clicking the AnimationPlayer and setting 'child nodes editable' makes no difference.
  • Scenario 2 : select an Animation, click the 'Animation' btn, select 'save as' resource. 'Load' that resource into a new AnimationPlayer, change the tracks a bit.
    Result : the Animation has also changed in the original AI scene.


So, I'll have to stick to my own solution for now ...

Exporting from Blender (take 2).

Up until now, I have been exporting models to OBJ. This was fine for me because I only needed the mesh and the materials.

Some things to be aware of when using OBJ :

  • If you want Godot to automatically apply materials to the models, you need to copy the .MTL file into the Godot resource folder BEFORE you copy the .OBJ file !
  • If you need your model to export as separate objects (like the lid and the body of the chest), you need to re-import the .OBJ file as a scene. This is a bit annoying as it requires a Godot restart.


Just recently, I decided to try exporting to glTF 2.0, a newer format.
This seems to work really well, even with the default export settings. I've been using the binary format (.GLB), but it seems that if you are using textures, 'glTF separate' might be a better option ( Godot docs).

Some things to be aware of when using glTF :

  • The materials you create in Blender have to be unique across all the models you will be importing into Godot.
    Alternatively, you might want to import each model + material into its own folder in Godot.
  • glTF uses PBR materials ... which means : node-based materials in Blender !


The final thing I wanted to mention here, is the (confusing) popup that shows up when you double-click imported models (.OBJ or .GLB) in Godot :

Popup 'imported scene'.

What it really means is this :

  • Click 'new inherited' and then select 'save scene as'.
    Result : all nodes are grayed out and can't be modified. But : you can add new nodes. This is a safety.
    If you later on decide to update the model in Blender (and export it using the SAME file name), all places where you have used that model in Godot will be automatically changed as well ! The nodes and scripts added to the inherited scene will all still be there and work as before (you may need to close and re-open the scenes in Godot to see the update).
  • Click 'open anyway' and then select 'save scene as'.
    Result : all nodes are editable. The link with the original imported model is broken (like a local copy of the original .GLB file).

GOAT

While I was still struggling to wrap my head around all of this, PaweĊ‚ Fertyk (Miskatonic Studio) gave a great talk about his framework for making 3d adventure games at GodotCon Brussels, 2020.

More information on GOAT (Godot Open Adventure Template) can be found on github. You should definitely watch his talk on YouTube to see all the things it can do, and how easy it is to make your own game with it !