I need help with my circle menu

I’m having problems getting my circle-oriented menu to rotate well (see below):

The icons move as commanded, but sometimes (depending on if there’s any lag on the PC), the icons do not move the entire angle because the coroutine ends before it can finish. Increasing the duration of the coroutine just makes the icons move slower and doesn’t guarantee that they go the full 51.XXXXX degrees that they have to go (there are 7 icons, so each one has to move 360/7 each time a left or right key is pressed). The final result is that the icons are lopsided and the selected icon is about 5 or 10 degrees to the right or left.

Also, when I move the circle of icons in one direction, then move it in another direction, the icons still move in the first direction and then switch to the new direction in a very jerky fashion.

How do I get the icons to move their complete angles without stopping, and how to I fix the jerky movement described above? I’m completely lost because I can’t figure out why its doing what it’s doing, so I can’t even begin to fix it.

Here is my complete code. It is self-contained:

using UnityEngine;
using System.Collections;

public class RingMenu : MonoBehaviour
{
	
	int mScale = 1, selection;
	float oX, oY, mX, mY, xCenter = (float)Screen.width/2, yCenter = (float)Screen.height/2;
	float iangle;
	float iiangle;
	float mAngle = 0.0f;
	float[] spriteX = new float[7];
	float[] spriteY = new float[7];
	bool jumpFlag, otherMenu;
	string outputString;
	public GUIStyle nhpFont;
	public Texture tItems, tSave, tLoaf, tOptions, tParty, tQuests, tQuit, tBlack;
	Rect rItems, rSave, rLoad, rOptions, rParty, rQuests, rQuit, rTest;
	Rect rBlack;
	
	void Awake()
	{
		nhpFont.contentOffset = new Vector2(xCenter, yCenter);
	}
	
	// Use this for initialization
	void Start () {
		iiangle = (360) / 7;
		for (int a = 1; a <= 7; a++)
		{
			iangle = (((2*Mathf.PI) / 7) * a) + (mAngle * Mathf.Deg2Rad);
			oX = Mathf.Cos(iangle) * mScale;
			oY = Mathf.Sin(iangle) * mScale;
			mX = xCenter + oX;
			mY = yCenter + oY;
			spriteX[a - 1] = mX;
			spriteY[a - 1] = mY;
			
		}
		rItems = new Rect(spriteX[0], spriteY[0], 40, 35);
		rSave = new Rect(spriteX[1], spriteY[1], 44, 42);
		rLoad = new Rect(spriteX[2], spriteY[2], 42, 42);
		rOptions = new Rect(spriteX[3], spriteY[3], 42, 42);
		rParty = new Rect(spriteX[4], spriteY[4], 42, 42);
		rQuit = new Rect(spriteX[5], spriteY[5], 42, 42);
		rQuests = new Rect(spriteX[6], spriteY[6],42, 42);
		rBlack = new Rect(0,0, Screen.width, Screen.height);
		StartCoroutine(MenuNavigate(iiangle * 4, 0, 0.5f));
		StartCoroutine(MenuExpand(1.0f, 100.0f));
	}
	
	//Main update function
	void Update()
	{
		
		for (int a = 1; a <= 7; a++)
		{
			iangle = (((2*Mathf.PI) / 7) * a) + ((mAngle - 90) * Mathf.Deg2Rad);
			oX = Mathf.Cos(iangle) * mScale;
			oY = Mathf.Sin(iangle) * mScale;
			mX = xCenter + oX;
			mY = yCenter + oY;
			spriteX[a - 1] = mX;
			spriteY[a - 1] = mY;
		}
		if (!otherMenu) //If there are no other menus on the screen.
		{
			//iiangle*selection
			TransformSprite(spriteX, spriteY);
			if (Input.GetButtonDown ("Horizontal"))
			{
				if (Input.GetAxisRaw("Horizontal") == 1)
				{
					StartCoroutine(MenuNavigate(iiangle*selection, iiangle * (selection + 1), 0.2f));
					if (selection < 6)
					{
						selection++;
					}
					else {
						selection = 0;
					}

				} else if (Input.GetAxisRaw("Horizontal") == -1)
				{
					StartCoroutine(MenuNavigate(iiangle*selection, iiangle * (selection - 1), 0.2f));
					if (selection > 0)
					{
						selection--;
					}
					else {
						selection = 6;
					}
				}
				
				
			} 
			outputString = getMenuItem();
		}
	}
	
	//Get the selected menu item
	public int getSelection()
	{
		return selection;
	}
	
	string getMenuItem()
	{
		string menuItem = "";
		switch (selection)
		{
			case 0:
				menuItem = "Quests";
				break;
			case 1:
				menuItem = "Quit Game";
				break;
			case 2:
				menuItem = "Manage Party";
				break;
			case 3:
				menuItem = "Options";
				break;
			case 4:
				menuItem = "Load Game";
				break;
			case 5:
				menuItem = "Save Game";
				break;
			case 6:
				menuItem = "Items";
				break;
		}
		return menuItem;
	}
	
	//This is what moves the icons in the 360/7 angle. 
	IEnumerator MenuNavigate(float start, float end, float duration)
	{
		float startTime = Time.time;
		while (Time.time - startTime < duration)
		{
			mAngle = Mathf.LerpAngle(start, end, (Time.time - startTime)/duration);
			yield return 0;
		}
	}
	
	//This pushes the icons from the center of the screen in a spiral fashion
	IEnumerator MenuExpand(float start, float end)
	{
		float startTime = Time.time;
		while(Time.time - startTime <= 0.7f)
		{
			mScale = (int)Mathf.Lerp(start, end, (Time.time - startTime)/0.5f);
			yield return 0;
		}
	}	
	
	void TransformSprite(float[] x, float[] y)
	{
		rItems.x = x[0];
		rItems.y = y[0];
		rSave.x = x[1];
		rSave.y = y[1];
		rLoad.x = x[2];
		rLoad.y = y[2];
		rOptions.x = x[3];
		rOptions.y = y[3];
		rParty.x = x[4];
		rParty.y = y[4];
		rQuit.x = x[5];
		rQuit.y = y[5];
		rQuests.x = x[6];
		rQuests.y = y[6];
	}
	
	// Update is called once per frame
	void OnGUI () {
		GUI.color = new Color(1.0f,1.0f,1.0f,0.5f); 
		GUI.DrawTexture(rBlack,tBlack,ScaleMode.StretchToFill);
		GUI.DrawTexture(rItems,tItems);
		GUI.DrawTexture(rSave,tSave);
		GUI.DrawTexture(rLoad,tLoaf);
		GUI.DrawTexture(rOptions,tOptions);
		GUI.DrawTexture(rParty,tParty);
		GUI.DrawTexture(rQuit,tQuit);
		GUI.DrawTexture(rQuests,tQuests);
		GUI.color = new Color(1.0f,1.0f,1.0f,1.0f); 
		GUILayout.Label(outputString, nhpFont);
		GraphicsChange(selection);
	}
	
	//Switching from semi-transparency to opaque icons
	void GraphicsChange(int num)
	{
		switch (num)
		{
			case 0:
				GUI.color = new Color(1.0f,1.0f,1.0f,1.0f); 
				GUI.DrawTexture(rQuests,tQuests);
				break;
			
			case 1:
				GUI.color = new Color(1.0f,1.0f,1.0f,1.0f); 
				GUI.DrawTexture(rQuit,tQuit);
				break;
			
			case 2:
				GUI.color = new Color(1.0f,1.0f,1.0f,1.0f); 
				GUI.DrawTexture(rParty,tParty);
				break;
			
			case 3:
				GUI.color = new Color(1.0f,1.0f,1.0f,1.0f); 
				GUI.DrawTexture(rOptions,tOptions);
				break;
			
			case 4:
				GUI.color = new Color(1.0f,1.0f,1.0f,1.0f); 
				GUI.DrawTexture(rLoad,tLoaf);
				break;
			
			case 5:
				GUI.color = new Color(1.0f,1.0f,1.0f,1.0f); 
				GUI.DrawTexture(rSave,tSave);
				break;
			
			case 6:
				GUI.color = new Color(1.0f,1.0f,1.0f,1.0f); 
				GUI.DrawTexture(rItems,tItems);
				break;
		}
	}
	
	//When the menu is deactivated...
	public void removeMenu()
	{
		StartCoroutine(MenuNavigate(iiangle*selection, (iiangle*selection - 179), 0.5f));
		StartCoroutine(MenuExpand(100.0f, 1.0f));
		//yield return 0;
	}
}

Errr, change the while condition in your coroutine from ‘while time bladibla’ to ‘while angle > end’ (since your angle is going down)?

Kaj

The angle can go either up or down because the circle and turn in two directions depending on the key pressed. The angle you’re referring to, mAngle, is referenced and changed throughout the class. I can’t reference the angle in the while condition because it could be any angle; what’s important is what’s done to the angle.

It looks like the culprit in this is the Lerp (or LerpAngle in this case) function. Has it been known to cut off when the time is up regardless of whether its completed its interpolation or not. I image that would be a problem

while (
((start < end) (mAngle < end))
|| ((start > end) (mAngle > end))
)
{
bla;
)

so if it has to go down it stops when the angle is less than start and when it has to go up it stops when the angle is more than start?

Kaj

Oh it’s not the lerp by the way. The way your logic is structured, if the currentTime is over the duration you stop doing anything, which means you’ll always skip the last frame, which would push your lerp to (or over) 1.0.

Depending on your framerate this is more or less noticable.

Theoretically you should check if there’s still a partial lerp left before you cut off.
The while that checks if you reached your target value would account for that, although (depending on the implementation of LERP) it will overshoot a little.
So if the lerp actually extrapolates at a value over 1.0 you should Clamp your lerp value to 1.0.

Kaj

…and finally, the way you set it up it’s slightly more complex to get it to rotate the opposite direction halfways, for example the fact that you tell it to take a fixed time regardless of the angle it has to travel (if I scanned your code correctly - I didn’t really study it). Seems more appropriate to take a ratio of speed and angle to travel.

In this case I’m not sure I’d go the coroutine route. I know they tell you updates are expensive but a few items when you are using a menu shouldn’t kill your game, and would make the logic a lot easier. Then again, I’m a C coder, I don’t think in CoRoutines.

Kaj

It’s the opposite…using coroutines usually makes the logic much easier. Trying to use tortured logic in Update to get the same results makes for code that often becomes very difficult to read and debug before too long (usually with lots of nested if/thens and other unpleasantness :wink: ).

No, that’s impossible…Lerp, by itself, doesn’t interpolate anything over time and it can’t be “cut off”, because it’s a function that returns a value once, immediately. Have a look at MoveObject on the wiki for a rotation function that rotates a given number of degrees over a period of time and is guaranteed to always end up at the correct end point.

Also, using GUITextures for this sort of thing seems less complex than OnGUI, because you can just move them around with parenting and stuff like any other game object. The only problem is that they use viewport space for their coordinates, so a normal (non-square) screen would end up with a stretched circle. One way to have a correct circle would be to use another camera on top with a square viewport rect.

Actually, I started something kind of like this a while ago but never used it for anything…let me hack it into shape… OK, this should be put on an empty game object. You should make sure there’s a camera in the scene tagged “MainCamera”. The script adds a GUIText component to the empty object, which you can set up with the desired font/color/etc. The menuTextures and menuLabels arrays should be the same length of course. The distanceFromCenter variable is how far from the center in viewport space the objects are; .5 would be the edge of the screen. The Rotation function is directly copied from MoveObject, mentioned above.

var rotateTime = .25;
var distanceFromCenter = .2;
var menuTextures : Texture2D[];
var menuLabels : String[];

function Start () {
	// Add camera and make it square; make sure main camera doesn't have a GUIlayer
	gameObject.AddComponent(Camera);
	gameObject.AddComponent(GUILayer);
	var ratio = 1.0 / ((Screen.width+0.0)/Screen.height);
	camera.rect = Rect((1.0-ratio)*.5, 0.0, ratio, 1.0);
	camera.depth = camera.main.depth + 1;
	camera.clearFlags = CameraClearFlags.Depth;
	if (camera.main.GetComponent(GUILayer)) Destroy(camera.main.GetComponent(GUILayer));

	// Set up menu items
	transform.position = transform.eulerAngles = Vector3.zero;
	var menuItems = menuTextures.Length;
	var menuObjects = new GUITexture[menuItems];
	var itemPositions = new Vector3[menuItems];
	var degrees = 360.0 / menuItems;
	for (i = 0; i < menuItems; i++) {
		menuObjects[i] = new GameObject("MenuItem", GUITexture).guiTexture;
		menuObjects[i].transform.position = Vector3.up * distanceFromCenter;
		menuObjects[i].transform.RotateAround (Vector3.zero, -Vector3.forward, degrees * i);
		menuObjects[i].transform.localScale = Vector3.zero;
		menuObjects[i].transform.parent = transform;
		menuObjects[i].pixelInset = Rect(-menuTextures[i].width/2, -menuTextures[i].height/2, menuTextures[i].width, menuTextures[i].height);
		menuObjects[i].texture = menuTextures[i];
		menuObjects[i].color.a = .25;
		itemPositions[i] = menuObjects[i].transform.localPosition;
	}
	transform.position = Vector3(.5, .5, 0.0);
	guiText.anchor = TextAnchor.MiddleCenter;
	
	// Expand menu
	transform.Rotate(Vector3.forward*180.0);
	Rotation (transform, -Vector3.forward*180.0, rotateTime*3);
	var t = 0.0;
	while (t < 1.0) {
		t += Time.deltaTime * (1.0 / (rotateTime*3));
		for (i = 0; i < menuItems; i++) {
			menuObjects[i].transform.localPosition = Vector3.Lerp(Vector3.zero, itemPositions[i], t);
		}
		yield;
	}

	// Let user rotate menu item left and right using the Horizontal axis
	var thisMenuItem = 0;	
	while (true) {
		guiText.text = menuLabels[thisMenuItem];
		menuObjects[thisMenuItem].color.a = .5;

		var thisInput = 0.0;
		while (thisInput == 0.0) {
			thisInput = Input.GetAxis("Horizontal");
			yield;
		}

		menuObjects[thisMenuItem].color.a = .25;
		var moveDir = (thisInput > 0.0? 1 : -1);
		thisMenuItem = (thisMenuItem + moveDir) % menuItems;
		yield (Rotation (transform, Vector3.forward * moveDir * degrees, rotateTime));
	}
}

function Rotation (thisTransform : Transform, degrees : Vector3, time : float) {
	var startRotation = thisTransform.rotation;
	var endRotation = thisTransform.rotation * Quaternion.Euler(degrees);
	var rate = 1.0/time;
	var t = 0.0;
	while (t < 1.0) {
		t += Time.deltaTime * rate;
		thisTransform.rotation = Quaternion.Slerp(startRotation, endRotation, t);
		yield;
	}
}

@script RequireComponent(GUIText)

–Eric

Of course, after going to bed I realized the dumb part I overlooked in my own reasoning.
When you lerp the coroutine way you miss the final timestep, however, when you exit the while loop you know that you consider it finished, therefore you should set the angle to target.

//This is what moves the icons in the 360/7 angle.
IEnumerator MenuNavigate(float start, float end, float duration)
{
float startTime = Time.time;
while (Time.time - startTime < duration)
{
mAngle = Mathf.LerpAngle(start, end, (Time.time - startTime)/duration);
yield return 0;
}
// We’re done, make sure we don’t miss anything
mAngle = end;
}

As for

It’s the opposite…using coroutines usually makes the logic much easier

Well, perhaps, like I said, I am used to coding without them for the past 30 or so years.

The problem I have with CoRoutines is that you dissociate from the state of your object, which you need for things like being able to go back halfways.
I can see the use of CoRoutines for something that is fire and forget, but for something that’s interruptable beyond a plain “Stop the coroutine” you end up maintaining extra state just because it happens to be in a coroutine.
To me a coroutine is useful for independent behaviour, whereas this is more like dependent behaviour, which is always slightly harder to run asynchronously.

I’m probably just to oldskool for it.

Kaj

Check out the code I posted; the Start function is the entire program flow, in order, from top to bottom. You’re doing:

  1. Set stuff up
  2. Expand menu
  3. Wait for input
  4. Rotate right or left based on input
  5. Loop back to 3

Everything is a linear flow in the order you’d expect, with no bouncing around between different functions with interdependent variables. For example, there’s no need to apply rotational calculations every frame when you’re just sitting there waiting for input, and no need for conditional “are we currently getting input or in the process of rotating?” checks. It makes event ordering a lot simpler…if I wanted an additional event to take place between the “expand menu” and “wait for input” parts, I can just stick another routine in there, without having to touch the rest of the code or worry that I’m messing up any dependencies. And code that is completed is just gone and done with, so for example I don’t have to worry that the “expand menu” code is still being checked every frame in Update, when the menu has finished expanding and I don’t need it anymore.

As far as Lerp goes, t is clamped between 0 and 1. In the Rotation function I have, the last loop in the while loop will always end up with t being slightly over 1, which is clamped to 1, so you’re automatically left with the last rotation value being exactly equal to the requested ending value.

–Eric

Well, I guess it’s a matter of taste. I find it hard to fathom how this would deal with ‘pressing left while it’s rotating right’. You’d need to cache the current rotation somewhere. If you’d then want to add an ease in and an ease out to that you add more state - I’d prefer to do it differently, but as I said, it’s a matter of taste/what you’re used to I guess.

Maybe it’s trivial - I probably have been working to long with sequential stuff and task based parallelism.

Kaj

I don’t see that you’d want to do that in this case, but if you did for some reason, indeed you’d do it some other way.

No, you just change this line:

thisTransform.rotation = Quaternion.Slerp(startRotation, endRotation, t);

To this:

thisTransform.rotation = Quaternion.Slerp(startRotation, endRotation, Mathf.SmoothStep(0.0, 1.0, t));

–Eric

I do see your logic, sourcerer. I’m still new to coroutines, so I still can’t say for sure on their usefulness. Your clarification was very helpful though. Thanks

As for Eric’s help. That’s interesting. I go through the Wiki periodically for useful scripts, but MoveObject didn’t look useful for me - I guess I simply didn’t know what exactly it did. I love how smooth everything looks.

I also see your script doesn’t use Update at all. Perhaps my dependency on Update was another issue in my code. In my defense though, I had no idea what I was doing when I wanted to make a circle menu. :stuck_out_tongue:

Thanks for your snippet and for directing me to MoveObject. I will tweak so that it’s in my native C# language. One more thing though: GUIText isn’t expected to be deprecated anytime soon right? We’ve all been pushed to used OnGUI since it came out so I to make sure I won’t have issues with GUIText in the future. :stuck_out_tongue:

Thanks for the enlightenment. Okay, ease-in was a bad example. I meant, “if you want to add in some state-dependent subbehaviour that is not necessarily sharing the timeline of the main behaviour in the coroutine”. Say a little bobbing in the scale, or a little icon spin at the start of the move.
But again, I guess if you’re comfortable with coroutines there’s an obvious solution for it, and when you’re not comfortable with them they feel restrictive.

It was one of his original problems.

"Also, when I move the circle of icons in one direction, then move it in another direction, the icons still move in the first direction and then switch to the new direction in a very jerky fashion. "

Kaj

Right, it’s not doing that right now, but that’s probably because there is no lag. I’ll see if I can get it to lag and see what it does then.

EDIT: Well, there’s slight jerkiness - barely noticable unless you’re looking for it. I’m not sure if it’s a big enough problem to require fixing, but if anyone has ideas they’re welcome to share. :slight_smile:

EDIT2: To clarify, the jerkiness described in the first post happened while the circle menu was not moving. So if I stopped rotating the circle, waited 1/2 a second, then rotated it in the other direction, it would jerk around. Now if I do that, it doesn’t do that. It only jerks around if there lag and if I’m rotating the circle and then change directions without stopping. This is probably because Eric’s code is so much smoother and lighter than mine.

That’s fine, I frequently have no idea what I’m doing. :wink: My general rule is:

If things need to be ordered or scheduled in a certain sequence, use coroutines.
If the same things need to happen every frame, use Update.

For example, first-person shooter controls, where you can move the view around with the mouse, and move the character in any direction plus jump, crouch, reload etc. at pretty much any time. In that case you check and do stuff every frame, and using coroutines doesn’t make much sense for that.

On the iPhone, GUIText/Textures are pushed somewhat over OnGUI, because of less CPU overhead, so I kind of doubt they’d disappear. Honestly, I’m not really satisfied with either method. OnGUI is not very well suited to some object-oriented GUI tasks, like this example shows pretty well I think. But GUITextures also have issues (aside from being totally unsuitable for complex systems)…one of them being viewport coords, which are good for some things, but see how I had to hack around with an extra camera with a square viewport to get them to work the way I wanted in this case. I want an all-new, really good 2D GUI system that’s not like either OnGUI or GUITextures, where it’s fast, reasonably flexible, resolution-independent as desired, and simple to use. Surely not too much to ask. :slight_smile:

That’s actually one reason why I used a coroutine, so you can easily and effectively block/unblock input with no hacky if/then logic all over the place. Using a coroutine like I did bypasses the problem entirely, since with the loop I used it’s not possible to get any user input while the icons are rotating, because that seems undesirable in this particular case.

–Eric

That is a solution indeed, but I could see I’d prefer the user to be able to rotate back if he changes his mind or made a mistake.
I would do it without if/then’s though, I’d just keep track of a desired angle and rotate towards that. It adds the benefit that if the player steers left quickly 6 times it’ll actually rotate right (shortest path to target).

It really depends on how you frame the problem I guess, and, again, how comfortable you are with coroutines.

Thanks for sharing :slight_smile:

Kaj

If the rotation was longer than .25 seconds I’d agree; as it is, by the time you change your mind it’s already finished rotating anyway. :slight_smile:

–Eric

I have another question.

I’m trying to convert to C#, could you tell me what this line means/does (it’s at line 61)?

var moveDir = (thisInput > 0.0? 1 : -1);

I can honestly say I’ve never seen that before. :stuck_out_tongue:

That’s a ternary operator. Basically it’s saying “Is thisInput > 0.0? If so, then use 1, otherwise use -1”.

You can do this the long way:

int moveDir = 0;
if (thisInput > 0.0) {
   moveDir = 1;
}
else {
   moveDir = -1;
}

Or the short way:

int moveDir = (thisInput > 0.0? 1 : -1);

I don’t think you need the parentheses in this case, but IMO it makes it clearer, and in some cases you do. So I generally use them.

–Eric

Cool, thanks. I have two more questions (or errors, however you want to look at it), and then this will work!

if (camera.main.GetComponent(GUILayer)) Destroy(camera.main.GetComponent(GUILayer));

As is, I guess this error: CS0176: Static member `UnityEngine.Camera.main’ cannot be accessed with an instance reference, qualify it with a type name instead.

So I do what it says:if (Camera.main.GetComponent(GUILayer)) Destroy(Camera.main.GetComponent(GUILayer));

refer to type Camera and then I get this: CS0119: Expression denotes a type', where a variable’, value' or method group’ was expected.

I don’t know what it wants, and also, I have trouble with this line:

menuObjects[i] = new GameObject("MenuItem", GUITexture).guiTexture;

It looks like is creates a new GameObject, and immediately access the Texture property. I get this error and I don’t know why: CS0119: Expression denotes a type', where a variable’, value' or method group’ was expected.