As the metaverse emerges as a means of digital interaction offered by enterprises, CSPs are now at the stage of strategizing about how to capture the benefits of this technology to better serve and support their customers. LotusFlare’s Metaverse Storefront lets CSPs create the same experience in the metaverse as in the real world when purchasing, onboarding and receiving ongoing service and customer support.
The focus of this blog is on another area of the storefront, namely, the game area intended to drive user engagement outside of the commercial and support areas. I am a development lead for our Metaverse storefront and, before getting into the work of advancing its capabilities, I assumed that the game area would be simple and fun to build as it appears on the surface. But I was wrong. From a development perspective, the game area was demanding and challenging for our team and there is a lot of complexity behind the curtains.
I’d like to share my experience by diving into the process of developing a game used within a metaverse environment, with all its advantages and obstacles on the way. Hopefully, it will help you to avoid some of the challenges our team faced so you develop better applications within metaverse environments.
To improve out ability to retain users in our metaverse environment, we thought to add some gaming elements – a “place” where they can take a break from the rest of the commercial experience. Initially, we decided on archery and darts as well as a daily reward type of item, which became a spinwheel, to drive user engagement and retention.
But wait…how does one actually make a dart game in a 3D environment? Time to sit down at the drawing board.
After a bit of thinking, it became clear that both the darts game and the spinwheel could be performed by the a similar method of interaction. After all, a spinwheel (a rotating roulette-type wheel with rewards outlined in sectors) as well as a dart game are essentially identical in that they’re both somewhat skill-based. The spinwheel rotates while the dart board is stationary but they both follow the same rules. In a darts game, you get points for the section you hit and the spinwheel gives out the prize outlined in the section you hit. For now, everything seems straightforward except for the few things necessary to facilitate that:
- How to detect hits?
- How to generate sectors?
- How to detect which sector is hit?
Necessary Math Refreshment
In the most game engines, drawing arcs is easy and the 0-degree angle actually points upwards. But not in trigonometry.
Whereas normally we would think in clockwise fashion, trigonometry usually works counter-clockwise.
To explain, when we imagine the 90-degree angle, the left hand (Edge A, the edge going upward) is facing 0-degrees, and the right hand (Edge B, the edge going rightward) is facing 90-degrees. Well, not so in geometry. In geometry, the 0-degrees is lying on the positive side of the x-axis (to the right) and 90-degrees is on the positive side of the y-axis (upwards).
That being said, circle has four quadrants, Q1 is the upper-right one, Q2 is the upper-left one, Q3 is the bottom-left one and Q4 is the bottom-right one.
We decided to use Unreal Engine 4 for our metaverse project that can use C++ or Blueprints, which is a visual scripting language that actually invokes C++ code under the hood. C++ code can be exported to the editor as Blueprint nodes and usually the engineers are working with C++ (or Blueprint) code while the game/level designers are using Blueprint. Generally, Blueprints are good for quick prototyping of new game features/mechanics.
So without further ado:
As show in Figure 1 above, the DartTableActor is a simple StaticMesh with a number of “helper” collision boxes to identify points of interest. Due to the mesh model being rotated sideways by -90 degrees (or 90, to keep with the geometric theme), the “Right Point” (signifying 0 degrees) is on top, “Top Point” is on the left, “Left Point” is on the bottom, and “Bottom Point” is to the right. The other points are interesting only for the darts game as will be seen later through the text.
This one seems straightforward but, in Unreal Engine 4 (or UE4 from now on) which will do most of the heavy lifting for us (such as collision detection in this particular example), we don’t actually have intersection points but rather vectors. Kind of a “similar thing” but not at all. It does contain the (x,y,z) coordinates of the impact point.
To get a larger part of the headache out of the way, we decided to do everything in a 2D geometric/mathematical world. After all, a darts game is merely a circle, with points in it representing hits.
As already mentioned, we get the “Other” (other hit actor) from the HitEvent and break the HitResult to extract the necessary “Impact Point” 3D vector.
And that’s pretty much it for hit detection: we have some hit coordinates but since we already decided to work in a 2D math world, we’ll massage that Vector3D to extract the actual point, (which we’ll conveniently store in a Vector2D) which will be used later on for solving our subsequent problems.
Now that we have an impact point, we can remove issue #1 from our list and move on to the next item.
(Disclaimer: I envisioned this article to encompass a journey as the development went on, however at the time of writing – the thing is done and working so I’m recreating the journey from personal memory.)
Detecting Which Sector is Hit
This is where the meat of the problem lies. This was the most interesting part of the work involved and this is the answer to “how to make a darts game”. Or so I thought.
What I realized was that years of Computational Geometry or Computer Science background doesn’t really fill in this particular problem/hole. “Surely, it should have,” I thought to myself but alas, it does not. KD-trees, R-trees, convex hull algorithms, line segment intersections and dual planes all seemed powerless here. I had to (re)invent math I was terrible at 15+ years ago while I was at college.
(In case you’re wondering, a “dual plane” is a plane where points become line segments, and line segments become points; useful for finding “shortest/longest lines instersecting … / containing …” types of things and solving that kind of problems. Additionally, it’s going to be the line segment which transforms to the point with the smallest X and highest Y. You’re welcome.)
Fortunately, the answer wasn’t as elusive as I first thought and can even be performed with simple integer arithmetic. For a point to be inside a circular sector, it has to meet the three following tests:
Clockwise? Counter-clockwise? What is this and why is it starting to sound like the Mad Hatter is having another unbirthday party but with math invited?
To test if a vector v2 is clockwise to a another vector v1, do the following:
- Find the counter-clockwise normal vector of v1. The normal vector is at a 90 degrees angle to the original vector. This is straightforward to do: if
v1=(x1,y1), then the counter-clockwise normal is
- Find the size of the projection of v2 on the normal. This can be done by calculating the dot product of v2 and the normal.
projection = v2.x*n1.x + v2.y*n1.y
- If the projection is a positive number, then the v2 is positioned counter-clockwise to v1. Otherwise, v2 is clockwise to v1.
So even without having actually generated sectors, we can try this out with… *drumroll* 4 sectors which are actually the quadrants. We can make those by connecting the CenterPoint with the 4 helper points and…. see that this thing is actually working. So, time to make some sector generating
code, err, blueprint…
But of course, problems.
(In case you were expecting to see the sector generating code, that princess is in another castle)
Looking at some of the earlier pictures, the helper points are all visible even though in reality, from all of the ones on the actual circle boarder, only the “Right Point” is used. So what was the problem with the vector-reliant sector generation code?
Since this is being written after the gaming area was made, I have to confess I don’t really remember which particular reason was the one that made me ditch the whole vector approach. It’s really the main reason why I haven’t shown or walked you through that piece yet (in other words, we ended up ditching certain parts of the juicy math after pivoting away from it). But it was one of the following:
- Certain math operations always works in “absolute world” mode, instead of “relative local” mode.
Meaning, sinus of a certain degree is always going to be the same however certain vector operations produce different results (this will become clearer later).
- The hits don’t always register at 0 depth where the actual mesh is, (x, y, 0) but actually with a +/- 5 tolerance, depending on how fast it gets processed. Meaning, certain hits were being registered at for example, (x, y, 5) and certain ones at (x, y, -3) for example. Somehow that was messing up the clockwise tests.
- Actor rotation had a big say in this. I realized that I did not do this collision the “preferred UE4 way” but the preferred UE4 way simply isn’t greatly orientation-agnostic either. The exact same position returned different results. Meaning that if I hit the x-axis while the actor’s rotation was 0, it returned e.g. (50, y, z). When it’s rotated to 180, the position would be (-50, y, z). Curious indeed but I suppose that perhaps the preferred UE4 way should either not be so math-reliant and intensive, because, if it is, you should apparently just math it out yourself. Maintaining (and making) these little hairballs (my name for them, not generally accepted, I know) for various fixes was simply not worth keeping the vector approach around as will also be seen in the intermezzo/walking through and explaining some of the code.
Because we don’t lead with excuses and sad stories in LotusFlare but prefer examples, it’s time to walk through the code and see what we have so far.
Let’s focus and walk through this part:
And let’s focus on the “Convert 3D Vector to 2D Vector” and more specifically, “Which Sector Was Hit” methods.
Since we made a decision to actually use 2D math for this darts game, we simply use the (x,y) coordinates of where the hit occurred and, depending on how the actor is rotated, we’ll pin one axis and use the other two as the x,y. Then we’ll simply use those for further math to determine which sector was hit.
To determine which sector was hit, we just loop through two same-sized arrays containing sector start/end angles and check whether the HitPoint is inside the sector we’re currently iterating over. So far it’s not looking like what we described up there?
So, what happened here? Where and why did vectors and their clockwise checks go?
Well, the answer is simple. At first, the sectors were generated by using the helper points on the mesh to generate vectors that were start/end of sectors; everything was mostly done with angles and vectors were just being used to conform to math equations. However, when that ran into a brick wall, it was clear that we could just keep on using angles with slight fixes to the non-working parts rather than breaking/reworking already working parts just so we stay entirely true to the equations and the idea outlined in the start of the article. Welcome to the gamedev, where things change rapidly and scope rarely stays the same over the course of a whole week.
So what does this all do – apart from having the cute “these values actually work, and the unused ones are what math says should work, but doesn’t” part in it? Take a look at the circle drawing in the math refresher section, imagine a point anywhere on it and a line from center to that point. The angle between that line and the x-axis is the so-called “Beta angle”. However, this beta angle needs different massaging depending on which quadrant the point lies in so we need a CAST check.
After the CAST check has been performed, we know exactly how much to add to the previous beta angle which will result in the real angle between x-axis and the line from Center to HitPoint.
Using that angle, we can safely and surely assert which circular sector was hit and award points (in case of a darts game) or assign an award (in the case of a spinwheel)
Where are the vectors and the juicy vector math? In fact, there was no need for them. They were a byproduct of all the angle math but ended up being unnecessary because we could pull off everything with just angles.
Why talk about a CAST check here when the reality is that we’re only comparing HitPoint’s (x,y) with the CenterPoint’s (x,y) coordinates? It used to be a CAST check, which was broken for the same “absolute world” math reason that broke vectors too. This needed either reworking already-working code with a lot of hairballs and keeping the “broken” code or fixing/replacing the broken code. After some analysis, it was determined that there was no need to stick to vector math since angles are already present everywhere and get the job done just as well.
Sector generating code
You’ve probably thinking by now “Wait so if we’re just doing everything with angles, why don’t we just loop around the circle border and generate sectors that way?”. And you’re right, that is what we’re just going to do, we’ll determine the “loop angle” by 360 / NumberOfSectors, and just … skip creating a normal (or a vector) to represent the sector start/end and just use the angle. We don’t need the left arm/right arm nor the clockwise checks. That’s the right way to do it of course, a mathematically correct way too but this one isn’t wrong either.
And more precisely, this part:
And that’s what it is since the number of sectors is parametrizable (e.g., it could be 8 for the spinwheel, but it’s 20 for the darts game) we simply divide up the circle by the number of sectors, get the “loop angle”, fill up the “Sector Angles” array and simply divide it up into sector start/end angle arrays – and boom, done. No need for spicy interesting math. It’s been cut out and abandoned once the whole vector approach was abandoned too.
Is this all?
Yes – in the sense that this answers and solves the three main problems outlined above. No – because you have already seen that there’s a lot more code in the Blueprints besides these crucial parts. So, why not go through the rest of it as well and clarify them.
However, the only remaining part(s) are the points/rewards code but to get a better grip on that we should remind ourselves with the core scoring rules of the darts game.
Let us first take a look at the big block with many outputs that happens in the initialization block – BeginPlay event.
And more specifically, this part – “Finalize adjustments based on WorkingMode”
As already noted, the WorkingMode is either a “DartGame” or “Spinwheel”, a simple enumeration value. If you’re wondering why or it’s not obvious, the answer is pretty simple – why have two things doing two similar-yet-pretty-much-identical things, when we could have one thing doing both. So this WorkingMode is there to slightly tweak a smaller portion of it’s behaviour.
As we can see, a number of things are happening here:
- for darts: the 2x, 3x and bullseye distances are calculated.
- for darts: the sectors are reassigned to be in the order they appear on the table (because they’re generated as 1-20, starting from the X axis, and their order on the table is different).
- for darts: the sector angles need slight adjustment, because the sector start line isn’t actually on the X-axis; the X-axis is somewhere in the middle point of that sector. So the angles are adjusted by roughly 1/2 of the sector arc “length”. (endAngle – startAngle) / 2 to be more specific.
- for spinwheel: show the spinwheel graphics over the dart board.
After that, the relevant point(s) radii are output from the function.
We can now take a peek at what happens after a hit has been detected and processed into which sector is hit.
Since we already calculated the radii (radiuses / distances of the helper points you can see on the first image) of the points of interest represented by miniscule collision boxes, we can perform a distance check between the CenterPoint and the HitPoint, compare it with the distance(s) of these points of interest and know whether we hit 2x, 3x or bullseye distances. Then we can pair that up with the actual point value amount of the region (circular sector) and award the actual expected amount of points.
Finally, deduct the scored points (if possible) from the leftover point amount, spawn a floating text saying how many points you won and. if the remaining points are 0, change the leftover point label to “You won”.
A similar process occurs for the spinwheel. However, since the text that comes with those is dynamic (as the rewards are pulled from the server), we have a private method that does that for the reward pulled from the prefetched rewards array.