It’s been a while since the last article I published. I was busy wrapping up a project I’m working on currently, which, as most game development projects do, turned out to require a little more attention than was planned.
The project I was working on was my first porting gig. As can be expected, this exposed me to a bunch of problems I never considered before. One of the tasks I was assigned was writing up a radial menu control, and that’s what I would like to write about today.
So, I wanted my radial menu to offer behaviors without enforcing any looks. Radial menu elements could be placed anywhere and look whatever the artists wanted them to look. To achieve this, I used the game engine’s systems configuration functionality. There, the designers were able to specify both the angle at which each item would start, and how big the angle span of each item would be. This was reasonable enough, but turned out to be a bit too complicated to set up, so in the end I scaled it down a notch. Still, the idea behind it was solid and it would have allowed designers to set radial menus up however they wanted them to be.
Next up was the idea that each radial element should have an angle span assigned to it in code. While there was nothing particularly harmful in that (except for the hardcoded angle span values I already mentioned), I found it more manageable to move the angle spans up one level. Instead of the basic radial menu structure being “radial menu -> radial item”, I decided to introduce an intermediary in the form of a radial slot, resulting in “radial menu -> radial slot -> radial item”. This let me achieved two goals:
If you have not yet completely fallen asleep, you might have noticed I listed processing analog input as something the radial menu doesn’t do. In fact, none of the radial menu parts I listed seem to have access to this information. Well, this is where the last piece of the puzzle comes in.
In my case, I was able to contain all the logic connected to angle-feeding the radial menu its angle in the base radial menu host class. This has turned each specific implementation of the host class into a sort of a radial item factory, with custom behaviors if needed. The base host class contained an AnalogInputProcessor (AIP) object that would calculate the stick angle on every tick. The AIPs functionality was somehow configurable, because in some cases designers wanted the logical direction to ‘stick’ to last direction for some time after the stick was released. The configuration of this object also made it possible to specify whether the radial menu would be fed the left or the right analog stick angle vector. This turned the AIP class into a sort of a state machine, where input would either be processed as neutral, active or sticking to last angle. Thanks to this state machine, all the host class had to do was to tick the AIP and retrieve current angle from it, no conditions and no questions asked. It would then pass that angle directly to the radial menu control it hosted, again, without any modifications. I recommend you do the same for your radial menus.
The radial menu host contains all its controls (radial items and an optional angle indicator) as templates and instantates them on initialization. Then, those elements get passed to the radial menu. A radial item would get passed on to a slot, which would be then rotated to the correct position. An angle indicator would be placed in its socket in the center of the radial menu control. The host would also set the angle span that the radial menu would have available for its items distribution.
The other problem that popped up was that of animating each individual radial items position/rotation. I couldn’t quite figure a nice way of animating the position of the radial slot, which would in turn animate the position its radial item child. Since the position of the radial slot was simply enforced on it during radial menus item distribution, I wanted to avoid having to store it in the slot or otherwise modify. Instead, I opted for animating each radial item child directly in relation to their 0,0 position (the center anchor of the radial slot). This way I didn’t have to store neither the slots position nor rotation. This was not ideal the most ingenious of solutions and if I had to point to one thing I’d improve in the future, that would be it.
Radial menu
At its core, a radial menu is nothing more than a UI control that consists of a circular items layout and an optional direction indicator. Seems simple enough, right? Well, as it often is with simple concepts, there is a ton of things that go into writing a radial control that is responsive, easy to use and flexible enough.Initial guidelines
Let’s start by quickly going over some of the initial discussions I had with people who had done this sort of work before. It’s very important to go through this, because without this context some of the decisions I have decided to take can be difficult to understand. Our initial design consisted of 3 different radial menus, two of them being half- and one being a full-circle. The strategy that was suggested to me was quite simple. It assumed that:- The visual side of each of the radials would be a set of elements, visually identical to how the specific control should look like in production,
- Each of the elements of the complete radial menu asset would be assigned in code to specific, hardcoded angle ranges,
- An element of the radial would get highlighted when the angle value of the analog stick vector would fall into the elements angle span
- In code, I would write a single ExecuteRadialAction function that would branch into multiple instructions, each of them hardcoded to specific angle ranges
My way
First of all, I refused to hardcode any values in code. It was bound to start festering sooner or later. It also stood against the principle of designing lookless controls, which I wanted my radial menus to be. If you are not familiar with the concept, I encourage you to read this article. Here’s mine, somehow shorter take on this: A control’s setup should be flexible enough to allow designers to change the look of every single element that the control consists of. A Button control that can be found in WPF offers behaviors, not looks. It can look any way a designer wants it to look like and it will still retain its functionality.So, I wanted my radial menu to offer behaviors without enforcing any looks. Radial menu elements could be placed anywhere and look whatever the artists wanted them to look. To achieve this, I used the game engine’s systems configuration functionality. There, the designers were able to specify both the angle at which each item would start, and how big the angle span of each item would be. This was reasonable enough, but turned out to be a bit too complicated to set up, so in the end I scaled it down a notch. Still, the idea behind it was solid and it would have allowed designers to set radial menus up however they wanted them to be.
Next up was the idea that each radial element should have an angle span assigned to it in code. While there was nothing particularly harmful in that (except for the hardcoded angle span values I already mentioned), I found it more manageable to move the angle spans up one level. Instead of the basic radial menu structure being “radial menu -> radial item”, I decided to introduce an intermediary in the form of a radial slot, resulting in “radial menu -> radial slot -> radial item”. This let me achieved two goals:
- It was even more apparent that a radial item is both lookless and positionless,
- Radial menu got its dependencies simplified a fair bit, since it didn’t even have to include a base radial item class anymore
The core responsibilities
To better understand the structure of the radial menu as I envisioned it, let’s break down the functionality of each part the control consisted of.Radial menu
It’s the container in which children are arranged circularly around its center.What it does | What it doesn't do |
---|---|
Evokes events on its children (like press or highlighting) | Process analog input (more on that down below) |
Spreads its radial slot children equally in either a circular, or a custom fashion | Initializes or builds its children visually |
Gives access to currently highlighted element | |
Places an optional angle indicator into its socket in the center of the control and rotates it |
Radial slot
It’s the container or a socket of the actual radial item control.What it does | What it doesn't do |
---|---|
Interfaces the angle span of an individual radial item | |
Acts like a socket into which an actual radial item is inserted | |
Locks to rotation of its radial item children on radial controls items distribution |
Radial item
It’s the element that represents an actual piece of the pizza, if you will.What it does | What it doesn't do |
---|---|
Processes UI events | Deals with its position and rotation |
Processes any logical callbacks if necessary | |
Looks! |
If you have not yet completely fallen asleep, you might have noticed I listed processing analog input as something the radial menu doesn’t do. In fact, none of the radial menu parts I listed seem to have access to this information. Well, this is where the last piece of the puzzle comes in.
Radial menu host
Since the radial menu itself is designed to be completely reusable regardless of the context, it has to have a parent element that feeds it the data it needs. Let’s call this element a “radial menu host”, or “host” for simplicity’s sake. The host has two main responsibilities:- Feeds radial menu the current angle of a stick vector,
- Instantiates the radial items and feeds them to the radial menu along with their angle positions as defined by desingers in the hosts configuration
In my case, I was able to contain all the logic connected to angle-feeding the radial menu its angle in the base radial menu host class. This has turned each specific implementation of the host class into a sort of a radial item factory, with custom behaviors if needed. The base host class contained an AnalogInputProcessor (AIP) object that would calculate the stick angle on every tick. The AIPs functionality was somehow configurable, because in some cases designers wanted the logical direction to ‘stick’ to last direction for some time after the stick was released. The configuration of this object also made it possible to specify whether the radial menu would be fed the left or the right analog stick angle vector. This turned the AIP class into a sort of a state machine, where input would either be processed as neutral, active or sticking to last angle. Thanks to this state machine, all the host class had to do was to tick the AIP and retrieve current angle from it, no conditions and no questions asked. It would then pass that angle directly to the radial menu control it hosted, again, without any modifications. I recommend you do the same for your radial menus.
The radial menu host contains all its controls (radial items and an optional angle indicator) as templates and instantates them on initialization. Then, those elements get passed to the radial menu. A radial item would get passed on to a slot, which would be then rotated to the correct position. An angle indicator would be placed in its socket in the center of the radial menu control. The host would also set the angle span that the radial menu would have available for its items distribution.
How it felt in use
I’m happy to report that this approach has paid off in production. It was possible to implement radial menu controls of various looks and feels, almost completely configurable without code support. One port that felt a bit off to some other developers was the radial slot. Apparently, its inclusion made the whole structure feel a bit bloated and it was easy to forget the slots purpose. I never had this problem, but then, I was the one to come up with the idea.The other problem that popped up was that of animating each individual radial items position/rotation. I couldn’t quite figure a nice way of animating the position of the radial slot, which would in turn animate the position its radial item child. Since the position of the radial slot was simply enforced on it during radial menus item distribution, I wanted to avoid having to store it in the slot or otherwise modify. Instead, I opted for animating each radial item child directly in relation to their 0,0 position (the center anchor of the radial slot). This way I didn’t have to store neither the slots position nor rotation. This was not ideal the most ingenious of solutions and if I had to point to one thing I’d improve in the future, that would be it.
Comments
Post a Comment