Sign in to follow this  

[C#] Ingame context menu

This topic is 2129 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hey guys,

I'm working on a RTS like game and just implemented context menus. When you click on a tree or anything a context menu will popup with the available options. It works, but I'd like to know if it could be done nicer / better.

I've got a ContextMenu class where the drawing and events are handled, a Tree class to show how it's used and a part of Game1.cs:

ContextMenu.cs:
[code]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace MyGame
{
public class ContextMenu
{
public static int minMenuWidth = 64;
public static int minMenuHeight = 64;
public static Sprite contextMenuBackground;
public static Sprite contextMenuBorder;

private Game1 theGame;
private Vector2 position;
private bool visible;
private GameObject target;

private List<ContextMenuItem> itemList = new List<ContextMenuItem>();

private int width = minMenuWidth;
private int height = minMenuHeight;

public ContextMenu(Game1 g, List<ContextMenuItem> items)
{
theGame = g;
foreach (ContextMenuItem item in items)
{
if (9 * item.getText().Length > width)
width = 9 * item.getText().Length;
}
itemList = items;
target = null;
}

public ContextMenu(Game1 g)
{
theGame = g;
target = null;
}

public bool onClick(MouseState mouseState)
{
Point mousePosition = new Point(mouseState.X, mouseState.Y);
Rectangle menuHitBox = new Rectangle((int)position.X, (int)position.Y, width, height);
if (menuHitBox.Contains(mousePosition))
{
for (int i = 0; i < itemList.Count; i++)
{
ContextMenuItem item = itemList[i];
Rectangle hitBox = new Rectangle((int)position.X + 10, (int)position.Y + 8 + 12 * (i), width - 20, 12);
if (hitBox.Contains(mousePosition))
{
item.onClick(target);
return true;
}
}
return true;
}
return false;
}

public void setTarget(GameObject o)
{
target = o;
}

public GameObject getTarget()
{
return target;
}

public void updateHovers(MouseState mouseState)
{
Point mousePosition = new Point(mouseState.X, mouseState.Y);
Rectangle menuHitBox = new Rectangle((int)position.X, (int)position.Y, width, height);
if (menuHitBox.Contains(mousePosition))
{
for (int i = 0; i < itemList.Count; i++)
{
ContextMenuItem item = itemList[i];
item.setHovering(false);
Rectangle hitBox = new Rectangle((int)position.X + 10, (int)position.Y + 8 + 12 * (i), width - 20, 12);
if (hitBox.Contains(mousePosition))
{
item.setHovering(true);
}
}
}
}

public Rectangle getHitBox()
{
return new Rectangle((int)position.X, (int)position.Y, width, height);
}

public bool isVisible()
{
return visible;
}

public void addContextMenuItem(ContextMenuItem item)
{
itemList.Add(item);
}

public ContextMenuItem getContextMenuItem(int i)
{
if (i >= 0 && i < itemList.Count)
return itemList[i];
else
return null;
}

public void draw()
{
if (!visible || itemList.Count == 0)
return;

// bg
for (int y = 0; y < ((float)height / 64); y++)
{
for (int x = 0; x < ((float)width / 64); x++)
{
if (y * 64 + 64 <= height && x * 64 + 64 <= width)
{
theGame.spriteBatch.Draw(contextMenuBackground.getTexture(), new Rectangle((int)position.X + 64 * x, (int)position.Y + 64 * y, 64, 64), contextMenuBackground.getSourceRectangle(0), Color.White);
}
else
{
Rectangle sourceRect = contextMenuBackground.getSourceRectangle(0);
sourceRect.Width = width - x * 64;
sourceRect.Height = height - y * 64;
theGame.spriteBatch.Draw(contextMenuBackground.getTexture(), new Rectangle((int)position.X + 64 * x, (int)position.Y + 64 * y, sourceRect.Width, sourceRect.Height), sourceRect, Color.White);
}
}
}

// left top
theGame.spriteBatch.Draw(contextMenuBorder.getTexture(), new Rectangle((int)position.X, (int)position.Y, 8, 8), contextMenuBorder.getSourceRectangle(0), Color.White);
// right top
theGame.spriteBatch.Draw(contextMenuBorder.getTexture(), new Rectangle((int)position.X + width - 8, (int)position.Y, 8, 8), contextMenuBorder.getSourceRectangle(1), Color.White);

// left
theGame.spriteBatch.Draw(contextMenuBorder.getTexture(), new Rectangle((int)position.X, (int)position.Y + 8, 8, height - 16), contextMenuBorder.getSourceRectangle(4), Color.White);
// right
theGame.spriteBatch.Draw(contextMenuBorder.getTexture(), new Rectangle((int)position.X + width - 8, (int)position.Y + 8, 8, height - 16), contextMenuBorder.getSourceRectangle(5), Color.White);

// top
theGame.spriteBatch.Draw(contextMenuBorder.getTexture(), new Rectangle((int)position.X + 8, (int)position.Y, width - 16, 8), contextMenuBorder.getSourceRectangle(7), Color.White);
// bottom
theGame.spriteBatch.Draw(contextMenuBorder.getTexture(), new Rectangle((int)position.X + 8, (int)position.Y + height - 8, width - 16, 8), contextMenuBorder.getSourceRectangle(6), Color.White);

// left bottom
theGame.spriteBatch.Draw(contextMenuBorder.getTexture(), new Rectangle((int)position.X, (int)position.Y + height - 8, 8, 8), contextMenuBorder.getSourceRectangle(2), Color.White);
// right bottom
theGame.spriteBatch.Draw(contextMenuBorder.getTexture(), new Rectangle((int)position.X + width - 8, (int)position.Y + height - 8, 8, 8), contextMenuBorder.getSourceRectangle(3), Color.White);

// Draw the items:
for (int i = 0; i < itemList.Count; i++)
{
ContextMenuItem item = itemList[i];
if (item.isHovering())
{
Rectangle hitBox = new Rectangle((int)position.X + 9, (int)position.Y + 9 + 12 * (i), width - 18, 12);
theGame.spriteBatch.Draw(theGame.dummy, new Vector2((float)hitBox.X, (float)hitBox.Y), null, new Color(255, 255, 255) * 0.3f, 0f, Vector2.Zero, new Vector2(hitBox.Width, hitBox.Height), SpriteEffects.None, 0f);
}
theGame.drawShadedString(theGame.spriteBatch, theGame.defaultFont, item.getText(), new Vector2((int)position.X + 10, (int)position.Y + 8 + 12*(i)), Color.White, 1.0f, new Vector2(1.0f, 1.0f), Color.Black);
}
}

public void setPosition(int x, int y)
{
position.X = (float) x;
position.Y = (float) y;
}

public void setWidth(int w)
{
width = w;
}

public void setHeight(int h)
{
height = h;
}

public void show()
{
visible = true;
}

public void hide()
{
visible = false;
}
}
}
[/code]

Tree.cs:
[code]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;

namespace MyGame
{
class Tree : ResourceObject
{
public static int treeWidth = 16;
public static int treeHeight = 16;
public static double maxTreeHealth = 100.0;
public static ContextMenu contextMenu;

private Sprite sprite;
private Random rand;

public Tree(Game1 g, Level l, Sprite s, int x, int y)
{
// bla bla
}

public override void tick(GameTime gameTime)
{
}

public override void draw()
{
theGame.spriteBatch.Draw(sprite.getTexture(), new Rectangle((int)position.X, (int)position.Y, width, height), sprite.getSourceRectangle(), Color.White);
}

public override ContextMenu getContextMenu()
{
return contextMenu;
}
}
}

[/code]

The action that will happen when an item is clicked is defined in Game1.cs:
[code]

List<ContextMenuItem> menuItemlist = new List<ContextMenuItem>();
menuItemlist.Add(new ContextMenuItem("Chop tree", delegate(GameObject o)
{
if (o != null)
{
if (selectedAnts.Count > 0)
{
foreach (Ant selectedAnt in selectedAnts)
{
Point walkTarget = new Point(o.getX(), o.getY());
selectedAnt.setTarget(o);
selectedAnt.walkNear(walkTarget);
contextMenu.hide();
}
}
}
}));
Tree.contextMenu = new ContextMenu(this, menuItemlist);
[/code]

Is this a good way of doing this? Using delegates as parameters for menu items?

Another idea I had was the following:
Create a IContextMenu interface with 2 methods: getContextMenuItems() and contextMenuOnClick(ContextMenuItem clickedItem) a right-clickable gameobject will than implement this interface. The drawing and handling of the clicks are than handled within Game1.cs.

Thanks [img]http://public.gamedev.net//public/style_emoticons/default/smile.png[/img]
Bas

Share this post


Link to post
Share on other sites
In my current project I've defined a CommandData class which is a base type, and then a bunch of more specific classes inheriting from CommandData such as PickupCommandData or AttackCommandData. Then, my context menu items simply need a CommandData property attached, and I can support (pretty much) any Commands my game needs.

My ContextMenu has an OnItemSelect callback, which fires whenever you select an item from the menu, and the handler is passed the appropriate CommandData for processing.

There's nothing really wrong with using delegates in the way you are if you're happy with that. Though I personally don't like it because the code for handling the context menu actions would be scattered through the code. I prefer such things to be somewhat centralised.

Share this post


Link to post
Share on other sites
Sorry for the late response.

I agree with you on the last part, I like my code to be structured logically. So I think I'll go with my second idea, I'll implement ContextMenu and override contextMenuOnClick() something like this:
[code]
public void contextMenuOnClick(ContextMenuItem clickedItem)
{
if(clickedItem.getText() == "Item 1")
{
// do something
}
}
[/code]

Share this post


Link to post
Share on other sites
I personally don't like doing string comparisons for something like that, since it introduces a point of failure if you ever decide to change the text of the menu item. For example, renaming the item at a late date, adding punctuation or translating your app into a different language. Ideally, the actions in your game should be independent of the word used to display them on the user interface.

This is why enumerated types are very useful. It might seem like it's a less flexible design because you have to add a new member to the enumerated type each time you need to add a new menu item, but its really not that much of a chore, you already have to write the code that interprets the action, so what's one extra bit of typing?

Share this post


Link to post
Share on other sites
Sign in to follow this