Hi! You maybe remember the tutorial I did on creating a basic First Person Controller some time ago. I sat down to finally remedy the problems that this setup had:
.Instant acceleration - If you press/let go of a movement button the player immediately goes full speed into that direction/stops, which does not feel smooth.
Collisions with walls - Since there the Controller simply tells Armory to move the player forward, the physics engine does a sloppy job of dealing with running into walls. It feels jittery and the player can even clip through the wall under some circumstances.
Slopes - The worst case scenario of colliding with walls. The Controller moves the player into the sloped surface, only to then be pushed up by the physics engine. Super jittery, and with inconsistent movement speed.
Jumping- Not existent. The setup would have allowed for it to be added, but it wouldn’t have been good.
I accomplished all these goals while keeping using Logic Nodes, and I am going to show you the result and the things I learn along the way. This is not going to be a Tutorial, I think you will soon see why.
The funkiness of Armory
You probably know: Armory is not perfect. So there were quite a few unexpected hurdles I had to overcome during the creation of the new Controller. For example, did you know that the execution of Logic scripts is locked at 60 times per seconds? I did not. This results in the game showing you the same result of your Logic for a couple frames if you use on a Monitor with a higher refresh-rate than 60Hz, which I do, and you game runs at more than 60FPS as well. I think this was set up like this so the speed of the logic of the game would stay the same, regardless of its frame rate without any additional setup required. For my purpose I want to tie the “logic rate” to the frame rate, for which I needed to change a bit of Haxe code. Thanks to @BlackGoku36 for helping me find this bit.
In iron/Sources/iron/App.hx line 46 has to be deactivated:
// kha.Scheduler.addTimeTask(update, 0, iron.system.Time.delta);
update(); added in line 113. This makes my installation of Armory different than anybody else’s, I hope someone can create a Pull request which adds UI functionality to switch between these two behaviors.
By the way, the value of
iron.system.Time.delta is always 1/60. To get the real delta time of the program you need to use
iron.system.Time.realDelta;. Of course the Time node uses
.delta, so I have to change that as well.
For the program to work I needed to get the position where the Player collided with a wall, so I created a custom node for that. It will be available in logic_pack.
Another thing that left me scratching my head is that reroute knots, aside from being an incredibly useful tool to organize your nodes, sometimes have the side-effect of breaking your program. When using a reroute knot with the modified Time node the value returned by the knot was not the time between frames, no no, it was the time for how long the game has been running for. I have no idea why, all I could do is avoid this case. Similarly when using knots to route the
Cast Physics Ray -> Normal output, it sometimes becomes NaN. Again no idea why, just don’t do it.
On the subject of
Cast Physics Ray -> Normal, when using this output to calculate something multiple times in one go the time the program needs to calculate the logic drastically increases, the longer this calculation is done the worse it gets Kind of as it would recourse and add a loop every frame. To “fix” this I set the value of a Vector to the value of the Normal once, and then for then keep on calculating everything using this saved Vector.
Creating the Logic
My idea was to keep the whole thing as mathematically as possible, so I have full control over everything that is going on. But I still needed to apply a Rigidbody to the player-object to get information on collisions and to also handle minor bumps on the ground, for which the old method of letting the physics engine push up the Player still works. Every frame the Physics-velocity is set to 0, so any freak collision won’t cause the Player to slide away.
The Player’s input is calculated into a raw movement vector. This already includes smooth acceleration.
Then a ray is cast downwards to figure out if the Player is grounded. If he is, the jump input is looked at and movement takes place. If he is not, “fake gravity” will take place.
Next up the raw movement vector gets rotated so it is parallel to the face the Player is standing on and still pointing into the correct direction.
After that it looks for a collision with a wall and then looks if the movement vector is pointing at the wall. If that is the case, the movement vector is “squished” so it becomes parallel to the wall, and the Player is being moved by that Vector. If not, this the Player is translated by the previous movement vector. The logic also covers the case that the Player is not currently colliding with a wall but will be placed inside one if the current movement vector would be used, as well as a few other edge cases.
Because the vector gets “squished” against the wall its length -> the speed of the Player changes, which needs to be updated in the Raw movement Vector.
Of course the Player needs to be slowed down if he is not pressing any movement buttons. That is take care of here:
This also takes care of not slowing the Player down while he is in air.
Maybe you noticed that I multiply Values a bunch of times by Time -> Delta. I do this to keep the speed of the Player constant, regardless of the frame rate of the game.
Downsides of this Controller
- It currently requires a modified version of Armory.
- This setup is only suitable in very static environments. Because the Physics-velocity is set to zero every frame the Player can not be pushed around by moving objects, including moving platforms.