Since I am working on an Escape Room setting where the player should be allowed to freely walk around in virtual reality without teleporting, the play area becomes hugely important. First the terms:
- Boundary: The actual line the player has virtually drawn to limit the available play space
- Play area: The biggest possible rectangle that fits inside the boundary
My actually very simple goal is now to create the world inside the play area. It cost me multiple days (ok to be fair, I also did solve tons of other problems in parallel, but this one really kept me busy).
Here is the catch: 0,0,0 can be anywhere and to make it even harder, the boundary can be rotated in any way around Y. The player can even dynamically change 0,0,0 by re-centering, e.g. long-pressing the Oculus button when using the Quest. I had actually hoped that VRTK would already solve this issue, but either I didn’t understand the concept or it does indeed not provide it.
The picture below illustrates the basic issue (with some debug output on top, “geometry” is the interesting part):
So my goal expanded: How can I calculate the correct rotation and offset to draw the world into the correct place, always being inside the purple boundary?
Given 4 points of a rectangle, it is possible to calculate the rotation using arc tan (I am not a math guy, so this took me a while). A good approach for both can be found at DashW’s GitHub. Centering is luckily merely a question of something like:
new Vector2((x1 + x3) / 2, (y1 + y3) / 2)
My approach is very similar (I found the above page only later unfortunately) except that I don’t rotate the camera but the constructed world. As mentioned in the comments on GitHub, the solution is actually deprecated. It turns out, Oculus added a new tracking origin type called “Stage” which does two things: it does the rotation calculation and will create the play area in a way that the rotation is zero. 0,0,0 will then be put into the center of the rectangle. That is convenient.
The catch is, it only works for Oculus Quest. If you have a Rift or other device or want to test locally, you are back to doing the math on your own. So I decided to actually keep my solution. I still set the tracking mode to Stage though as it brings one crucial benefit: it disables re-centering. The player cannot interfere with the play area any longer which is really important for scenarios where one needs a stable fit, e.g. placing furniture or, an Escape Room.
The next challenge was to identify the corner inside the boundary, that will represent X and Z pointing inside, so that the constructed world can be anchored there. I use a bit of a weird algorithm which I came up with after some trial and error and quite some automated tests.
// determine correct anchor point float rot = GetPlayAreaRotation().eulerAngles.y; int i = (rot >= 90 && rot < 270) ? (boundaries.x >= boundaries.x ? 3 : 2) : (boundaries.x >= boundaries.x ? 2 : 3);
As a result I get the index of the boundary corner which then is my offset for the world after rotation. Below an illustration of what happens when the wrong corner is picked.
Something that can really break the immersion in my opinion is when suddenly the Guardian system kicks in and reminds you that there is a physical world around you. This will happen automatically (and cannot be deactivated for non-developers) whenever the player gets close to it.
When constructing a world inside the boundary, this means that every time the user approaches a wall for example, the boundaries will pop up. To mitigate this, we can use the concept of padding by artificially reducing the size of the play area. This will of course make the gaming world slightly smaller but my measurements show that already 15 centimeters make a big difference.
A for me surprisingly interesting part was how to actually add the padding to the offset calculated one chapter earlier. I somehow had to move the point into X and Z direction.
After a bit of tinkering the solution is actually quite simple: Since we can calculate three points, the fourth one can be derived through (D being the unknown one):
AD = BC (Dx - Ax, Dy - Ay) = (Cx - Bx, Cy - By) Dx = Ax + Cx - Bx Dy = Ay + Cy - By
Putting it all together
Once the math was solved I built a UI (more on that in another blog) so the player can now adjust the settings to his needs. The grid is created with a couple of line renderers which seems to work out just fine so far and it actually has a nice glowy touch out of the box. There are alternative ways of drawing lines as described here but for now I won’t bother with that as I quite like the result.
Right now the player can only change the padding but the editor already supports adjusting grid size as well which I might work on in a future version (imagine having wider floors to walk through for more convenience). Currently without any scaling adjustments its more like an artsy room generator 🙂