Week 6
There is enough to cover this week to split it up into sections - camera follow, T-Shape task and grabbing.
Camera Follow
Last week we saw the hero walking further and further away from the player up to the point of unrecognizability. Solving this in a bare-bones way is easy enough.
I also tried following the hero's rotation.
Though ultimately I removed the rotation following as it caused a bit of motion sickness and felt less immersive1 to me.
Even though the above code (including rotation following) is fine, it caused eratic camera movement. In my control bar code, I have been using global coordinates, because the local coordinates stay fixed. In the scene structure below, you can see that the controlbar is parented to an XR controller, which in turn is parented to the XR origin.
Now, if we move the XR origin to follow the hero, we change the XR controller's and in turn the controlbar's global position. Because of the control scheme, this results in additional input and hero movement. This movement in turn necessitates that we move the XR origin to follow the hero. Rinse and repeat, we get a positive feedback loop, which is very motion sickness inducing.
My solution is to use coordinates local to the XR controller. I exploit the scene structure to achieve this with the following code attached to controlbar. For a more flexible approach, one could walk the scene tree instead.
It is split up in two functions, because I also use getLocalTransform for the hero's head and feet rotations.
T-Shape task
I don't like the way T-Shape tasks are implemented in the given parkour. There is no believable reason for why the tasks have to be completed and I don't like the timeouts and button interactions during the tasks. They make the task tedious without good reason in my opinion. This is why my T-Shape tasks deviate in two important ways:
- The task includes a wall preventing the player from progressing until the task is cleared.
- A T-Shape positioning is accepted as soon as it is close enough without any button confirmation.
The wall is well, just a wall. Instead of just removing it upon task completion, I lower it with the AnimationPlayer. Godot makes this easy, but I found no good way to stop an animation at its end. Instead I added an "animation"2 for the lowered state and queued it after the lowering animation.
"Close enough" needs a good metric. I split "close enough" into position and rotation similarity. For position I use the Euclidean norm (the usual spatial distance).
For rotation similarity, my first thought was to threshold the Euler angle differences. This though resulted in ugly code and having to think about too many edge cases, which led me to look into alternatives. I found out that quaternions are great for this. The math is nice, but Godot has an angle_to function, which hides all the complexity. One remaining matter: The T-Shape is rotation symmetrical. There are two rotations, which should be accepted. Quaternions to the rescue! Quaternions have the nice property that one can rotate them by multiplying them. I can just start with a quaternion describing 180° rotation and multiply the target rotation quaternion to it!
(One could write shapesAlign as a single condition, but it is easier for me to reason about it this way.)
distanceThreshold's and rotationThreshold's final values were determined empirically. Playtesting revealed that the threshold values should be a little more foregiving than the ones I deemed a good challenge for myself.
For making the T-Shape interactive I attach a collider and my CanBeGrabbed scene, which I explain in the next section, to the T-Shape.
Grabbing
Grabbing works with two scenes, CanBeGrabbed and CanGrab.
CanBeGrabbed does nothing except provide a node named "CanBeGrabbed" for CanGrab. It is a small footgun as its parent needs to be a CollisionObject3D with attached CollisionShape3D. Similarly, CanGrab must be attached to PhysicsBody3Ds with attached CollisionShape3D.
Since CanBeGrabbed does nothing, CanGrab does all the grabbing logic. CanBeGrabbed has two functions for responding to input, grab and release. grab collets a list of nodes, which should be moved along the CanGrab parent and release erases this list.
There are two things in the above code, which I still have to explain. First of all, note that I set collision_layer to 0 for grabbed nodes and restore it in release. It disables collision for grabbed objects. This is a hack for not having to think about what should actually happen when the player pushes a grabbed object against walls, the ground, etc.
Secondly, I store the initial node transform and the inverse of the initial parent transform. I need this information for actually moving the grabbed objects in _process. It would be possible to store the product, but I don't, because I think it would hurt readability.
is the transform, which would transform the CanGrab parent from the point of time the grab started to its current transform. Consequently, multiplying it with the initial CanBeGrabbed parent's transform will yield the transform of the CanBeGrabbed parent after all the movement of the grab until now happened.
The last step to grabbing is having something to attach CanGrab to and triggering grab and release. I will use the hero's hands. Grab and release will be called based on trigger position of the VR controllers. Binding controls is covered by Godot's documentation. Moving the hero's hands I will cover next week.
Footnotes
-
IARVR terminology would be "led to feeling a lesser degree of presence". Since I cannot control the hardware, the distinction between immersion and presence is not important for this blog. I prefer being able to use an adjective and "breaking immersion". ↩
-
Is a video with a single frame a video? Is a list with a single entry a list? When does a collection of trees become a forest? Is 4'33" music? ↩