Logo 3.5Cats_AndHalfAFish



October 04, 2020 Shade, Godot, 3d, Blender, slopes
The first staircase connecting two levels is in ... What a pain !

Note : Blender 2.83, Godot 3.2.1, Gimp 2.10.18.

Last week, I finally started working on stairs (not something I was particularly looking forward to). Creating stairs and making them work in game is a -big- project ...

The building blocks :
1) 3d modeling (making sure they fit in the alloted space)
2) collision shapes (making sure they're smooth and not too steep)
3) gdscript (walking up and down a slope)

Stairs : Blender.

The first thing that needed to be done, was to figure out how I was going to fit in the stairs between two particular floors. Since there's quite a bit of space to cover (9 m), and because the stairs cannot be too steep (*), I decided to fit in three relatively short staircases.
(*) I figured this out some months ago, in Godot, while experimenting with various kinds of slopes. I finally settled on a max slope of 3 units up over 5 units horizontal.

Max slope for stairs.
Maximum slope for stairs.
Blender : stairs blockout.
Blender : staircase blockout.

I also watched a bunch of videos on how to model stairs in Blender. They are mentioned in the list at the bottom of this page.
It's actually not all that difficult to model some basic stairs. It all comes down to the 'array modifier', and an 'empty' when you need to create spiral stairs.

Blender : stairs 3d modeling.
Blender : basic stairs 3d modeling.

I tried very hard not to have 13 steps in a staircase, but alas ...

I also needed to have some additional walls and ceilings to hide the stairs from view. After a while, my brain started to feel the way this wireframe model looks :

Blender wireframe model : staircases with walls and ceilings.
Blender (wireframe) : staircase with walls and ceilings.

And as if this mess wasn't bad enough, I still needed to add the collision shapes !

When dealing with collision shapes, it's important to remember some simple rules :
1) Keep the shapes as simple as possible.
2) Collision shapes have to be -closed- shapes ; don't remove faces that you feel aren't important !
3) Don't use planes, unless in some special cases. I read that in Godot 'plane' collision shapes are infinite !

Since my character is moving around at a rather sedated pace, a collision shape thickness of 0.1 Blender units works fine for me.

Blender : collision shapes for wall and stairs.
Blender : collision shapes for wall and stairs.

Stairs : Godot.

It has been such a long time since I did any coding in Godot, that I can't actually remember the code I used to have a player character walk up and down slopes. Time for a quick refresher ...
Also, I want to mention explicitly that my brain refuses to understand KinematicBody movement code. So, I'm pretty sure that the code I'm using is ... strange/broken/wrong. But it seems to work, so I'm going with it. 😎

Godot : staircase.
Godot : staircase.

OK. So. Some months ago, I settled on the move_and_slide() method of the KinematicBody node (player). The reason being that this lets the player 'slide along' obstacles (walls, etc) rather than collide with them and stop (feels nicer when you play).
The documentation tells me I need to call move_and_slide() from inside the _physics_process() function (something to do with the delta value being automatically taken into account). So, that's what I'm doing (through process_movement()).

Godot : code for moving up and down stairs (1).
Godot code : every physics frame, player input is converted into on-screen movement.
Godot : code for moving up and down stairs (2).
Godot code : moving fw, bw, left, right, up, down.

The move_and_slide() method has a whole bunch of parameters, but I'm only specifying the first 3 :
1) linear_velocity : the velocity vector (m/s). Important : do -not- multiply by delta (physics engine is handling this) !
2) up_direction : used to distinguish wall from floor and ceiling.
3) stop_on_slope : if the player character is standing still on a slope, and the linear_velocity includes gravity, he will start to slide down the slope unless you set this param to 'true'.

The rest of the params have the Godot default values, including the 'floor_max_angle' which is set to 45 degrees.
The move_and_slide() method returns the linear_velocity vector (adjusted for rotation and/or scale after a slide collision), but I'm not sure how to use this value ... So, I'm ignoring that.
You can also get more information about the collisions with get_slide_collision().

I'm also currently toying with the idea of changing the camera angle when the player character is walking up/down some stairs. The basic setup is like this :

Godot : setup for adjusting cam angle on stairs.
Godot : setup for adjusting the camera angle on stairs.

I have an Area node with a BoxShape CollisionShape spanning the entire stairs. When the player enters the Area, the 'is_on_stairs' variable (in the Player code) is set to true.

Godot : code for adjusting cam angle on stairs (1).
Godot code : script attached to Area parent node.

Note : instancing(*) nodes that contain collsion shapes is a bit tricky in Godot. Changing dimensions of 1 collision shape will also change the dimensions of all other collision shapes in the scene ! Also, since the shapes are shared between various Areas, methods attached to one area will also be triggered by the other areas !
(*) Root node > right-click > 'instance child scene (that contains a collision shape node)', OR, existing node (that contains a collision shape) > duplicate.

The reason for this behavior is that the 'Shape' used to define the CollisionShape dimensions is a 'Resource' (a data container) and not a node !
In Godot, all resources are shared. Meaning : if that data 'exists' somewhere in a scene, it will be reused whenever you try to add another Shape data container.

Godot : CollisionShape node with Shape resource.

So, to solve our problem, we need to set 'unique subresource' on the CollisionShape node.

Godot : make Shape unique.

This whole setup is not terribly convenient : the 'uniqueness' property is hidden in a dropdown menu, and for some reason it is not a checkbox. Also, it is not accessible from code.
So, there's not really an easy way to make sure that different shapes in the scene are not linked (unless you create the whole setup from scratch every time, which is also inconvenient).

My 'hack' is two-fold :
1) to no longer define a shape, so I'm forced to create one whenever I instantiate a zone for the stairs in my scene.
2) make a special group for these 'zones' so I can check in code if shapes have been shared somewhere (in case I accidentally duplicate a zone in a scene ... which I just know I will do at some point). You can do this by looping over all members of the group, and checking whether 2 or more CollisionShape.shape's are the same (if cs1.shape == cs2.shape). You can trigger this code for instance whenever you run your game from the editor (F5).

Godot : code for changing cam angle when on stairs.

Mmm ...
Anyway, this is how I'm using the setup in the Player code :

Godot : code for changing cam angle when on stairs.
Godot code : looking slightly down when walking on stairs.

If I decide to keep this feature in the game, I will of course need to refine it a bit. 🙂

This is what it currently looks like in Godot :

Versus 'regular camera' :

I think going down the stairs definitely looks better with the camera angle adjustment (maybe I should point the camera even further down). I'm still unsure about going up the stairs, as well as resetting the camera angle when the player pauses on the stairs ...
Fortunately, I still have plenty of time to decide on this. 🙂

This is why testing is important.

Walking up and down the stairs ...
Looking up ...
Huh ? Where's that hole coming from ???

Solved !
It's the floor ! It has backface culling enabled ! 😮

Relevant information.

  • Modeling straight stairs : tutorial by Wasiq Wiqar (YT).
  • Modeling straight and spiral stairs : tutorial by EMU Enoksi (YT).
  • Modeling spiral stairs : tutorial by CG Crafted (YT).
  • Modeling spiral stairs (computer voice, but great result) : tutorial by blended (YT).
  • Godot Resources : snippet.
    "Resource ... serves primarily as a data container. A resource is reference-counted and freed when no longer in use. It is also cached once loaded from disk, so that any further attempts to load a resource from a given path will return the same reference."
    "The resource 'duplicate()' method duplicates the resource, and returns a new one. By default, sub-resources are shared between resource copies for efficiency. This behavior can be changed by setting the boolean 'subresources' param to 'true'. This will create a shallow copy (nested resources within subresources will not be duplicated but shared)."