Monday, May 27, 2013

Monotouch Dialog Simple pattern.

public class PrintDialog : DialogViewController
    {
        string path;
        public PrintDialog (string Path) : base (new RootElement(""))
        {
            path = Path;
            this.Pushing = true;
            this.Root = GetUI ();
            this.RefreshRequested += delegate {
                this.Root = GetUI();
                this.ReloadComplete();
            };
        }
        public RootElement GetUI()
        {
           // Generate Your Root Element
        }
    }


I like having my Monotouch Dialogs refresh on drag.

I also like my navigation controller.  Decided I'd post a small bit of code without implementation details that makes me very happy.

Print Hub Update.

  

 Spent a bit of time on making the interface better for Print Hub.  Have a ton of features that are.. "Developed", but can't quite be released yet.  Yeah, it's 3:00am in the morning.  Life is brutal.

Sometimes, I feel like Thulsa Doom, talking to my inner Conan.



Monday, May 20, 2013

Building a Prototype - Part 1

We have an idea locked down for our first commercial release.  Design Doc follows:


Project Serious
A top down turn based tactical dungeon crawler, targeted to mobile platforms.

Gameplay:
Hub Screen: services to buy/sell, craft, access stash, recruit heroes and obtain jobs(quests)
Player is offered a selection of quests from a random quest list and must pick only one.  Dungeon areas for quest are randomly generated.
Player manages a party of up to 4 characters, chosen from a roster of heroes.  Heroes can be customised via points earned from levelling them up.
After picking heroes and equiping them, Player can enter quest area and embark on quest and main game begins.
Main Game:
Exploration phase: Player explores randomly generated dungeon. All non-combat actions take place in this phase. Can move each hero up to a maximum of 12 squares.  If enemy is spotted, exploration phase immediately ends and combat phase begins.
Combat Phase: All combatants(heroes and enemies) take turns to act.  Each unit has 'action points' they can spend on moving, attacking and performing special skills.
Combat Phase runs until either all heroes are dead, or all enemies are dead or subdued.  If all heroes die, player is returned to the hub screen.  He can recruit new adventurers to continue the quest, they can also loot the bodies of fallen heroes if they can make it back to the scene and defeat what killed them.
Death is permanent but Resurrection exists, for a price.  To return a life, a life must be given.
These phases run as needed until the victory conditions for the quest are completed and player receives quest reward + dungeon loot and is returned to the hub screen.
Player can upgrade his guild hall to attract more powerful heroes and to better equip new recruits.  Player can add apothecary, enchanters, blacksmiths, leather-workers, etc
CO-OP Mode.
Up to 3 other players can sub in their own heroes from their own rosters in your game, while maintaining the maximum of 4 heroes per adventure. 

Its one thing to write down an idea, and quite another thing to bring it to life.  Before we dive in and start seriously investing our time and resources to this idea, we need to make sure it's actually fun to play.  It sure sounds it on paper (at least to me), but the game execution may not be.  A prototype is needed.

To start we need something to look at.  I can reuse my grid code to store the level layout and the cube creation code from my Maze project to create the actual walls and floor.

In the maze game I used a 2D Int array to store the layout.  The values were always either 0 or 1.  0 denoted an open space (no cube), 1 denoted impassible space(create cube).  If I used this structure as is, it would work, but leave me no flexibility to add anything interesting to the dungeon - right now each 'grid cell' only contains enough information to know if they should be a wall or not.  What if there is a trap in this cell?  Or a chest full of loot?  Or the mysterious glowing well?  A secret doorway? We can create a new structure to store as much information as we need, and store that in a 2D array instead of using Int's. I made Code a read only property because I really don't want this variable being changed once set, but it will be often checked against.

public class GridCell : MonoBehaviour
{
public bool IsPassable; //If false, will be a wall.
private string code; //Will be used to determine any cell features
public string Code
{ get { return code;}}
public void SetData(bool isPassable, string code)
{
IsPassable = isPassable;
this.code = code;
}
}

The GridCell class.  I dont need to attach this script to an object - instances are created through code and stored directly in an array.  I created the following loop for a test run.  As each cell in the level is created, it randomly sets the IsPassable flag to either true or false.

for(int x = 0; x < Width; x++)
{
for(int y = 0; y < Height; y++)
{
       GridCell cell = new GridCell();
int i = Random.Range(0,2);
if(i == 0)
{
cell.SetData(true, string.Empty);
}
else
{
cell.SetData(false, string.Empty);
}
LevelLayout[x,y] = cell;
}
}

And the result with a structure that is 20 cubes high and 40 cubes wide...note the upside down cross in the top right middle of the layout... a sign?


Not much to look at, but it's proves my structure is functioning.  Next - applying some algorithms to make this an actual playable dungeon.


Saturday, May 18, 2013

Realtime Terrain Manipulation

Now that I have a game world split into cells, it can allow me to easily do some fancy things, such as targetted real-time terrain manipulation.  Think Populous where you would raise or lower the game world  or spells or technology that can cause a mountain or crater appear at the feet of an enemy (or player!), and have that mountain or crater become an actual part of the game level.

There are other things to focus on before this.  Camera setups, entity management, path finding - they take priority.  But I am going to revisit this.

I found a script on the forums.  Its in Java, I'll convert it to c# at a later date.
http://forum.unity3d.com/threads/87086-Realtime-Unity-Terrain-Manipulation-Possible?p=755093&viewfull=1#post755093

I ran the script as directed.  The end result is nothing fancy, just random heights being applied to a specific section of the map without recalculating the entire terrain.  That last part is important because recalculating the entire terrain mesh every frame is a massive performance hit.

All I need is a script to simulate curves...... mmmm curves...

Thursday, May 16, 2013

2D Grid on a 3D Terrain

Quick post tonight.

I wanted to mess about with an idea that has bouncing around my head.  Taking a Unity terrain and building a 'grid' over it, similar to what you would find in most tactical turn based games.

So to start:
Create a Terrain.  Resolution does not matter, the grid will adapt.
Create an empty gameobject, I called mine GridOrigin.
Attach the following script to it.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Grid : MonoBehaviour {

public Terrain terrain; //terrain grid is attached to
public bool ShowGrid = false;
public int CellSize = 10;

private Vector3 terrainSize;
private Vector3 origin;

private int width;
private int height;

private List<GameObject> objects;

void Start ()
{
terrainSize = terrain.terrainData.size;
origin = terrain.transform.position;

width = (int)terrainSize.z / CellSize;
height = (int)terrainSize.x / CellSize;

objects = new List<GameObject>();

BuildGrid();
}

void Update ()
{
foreach(GameObject obj in objects)
obj.SetActive(ShowGrid);
}

void BuildGrid()
{
for(int x = 0; x < width; x++)
{
for(int y = 0; y < height; y++)
{
GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube);
Vector3 pos = GetWorldPosition(new Vector2(x,y));
pos += new Vector3(CellSize / 2, terrain.SampleHeight(GetWorldPosition(new Vector2(x,y))), CellSize /2);
go.transform.position = pos;
if(x==0)
{
go.renderer.material.color = Color.red;
}
if(y==0)
go.renderer.material.color = Color.green;

go.transform.localScale = new Vector3(CellSize /2, CellSize /2, CellSize/2);
go.transform.parent = transform;
go.SetActive(false);

objects.Add(go);
}
}
}

public Vector3 GetWorldPosition(Vector2 gridPosition)
{
return new Vector3(origin.z + (gridPosition.x * CellSize), origin.y, origin.x + (gridPosition.y * CellSize));
}

public Vector2 GetGridPosition(Vector3 worldPosition)
{
return new Vector2(worldPosition.z / CellSize, worldPosition.x / CellSize);
}
}

Attach your terrain to the script.

When you run, you should see your terrain as normal:

In the GridOrigin object in the inspector, you can toggle a visual display of the grid on and off.  The cubes appear in the exact centre of the gridcell.  Green is X dimension and red is Z.

Each gridcell can be referenced by supplying an X and a Y local co-ordinate.  5,7 for example, refers to the grid cell that is 5 cells in the x dimension and 7 cells in the Z dimension.

There are two methods at the end of the script GetWorldPosition and GetGridPosition.  This is really the heart of the script - GetWorldPosition will return a Vector3 that corresponds to the passed GridLocation.  You can use this to position objects.  For example, if you wanted to move your skeleton object to the grid location 5,7 you can use skeleton.transform.position - grid.GetWorldPosition(5,7).  GetGridPosition will return the grid location of a passed Vector3 as a Vector2.

Using a loop you can quickly create and place a horde of enemies.  900 skeletons in this case...




Wednesday, May 15, 2013

And now for something completely different....

I've spent the last few weeks recreating simple game mechanics while learning Unity, with the goal of producing something that can be sold.  The fruit of this tinkering so far:

First person Angry Birds.  There be monkeys on top of them towers for you to send plumetting to their deaths.  All gameobjects are primitives.
Jumping Game - just jump as high as you can without falling.  Spiders and Fire will kill you.  Using free Asset Store models

Brains drop from above and you catch them in the crate.  You get a point for every brain collected.  Please note the excessive use of particle effects in this scene.  Using free Asset store models.


So far, basic games.  The core mechanic in each has the potential to be commercial successful, but only with significant investment in the art department.  You have to catch and hold the eyes and ears of the demographic likely to spend any kind of money for such a simple game.   The other key to success with this kind of game is the level of social interaction it provides - updates to Facebook, Twitter, etc.  This is an area I have zero experience in.

The next step is to consider some more sophisticated game mechanics.  Things like random level layouts add re-playability to a game.  I tried that out next, the result is this maze generator.  It's not really a 'maze' thats being generated, but I couldnt think of a better word to describe it.  If you want to follow along, I think it's a good start for your own levels or just learning how to do stuff if you are just starting to peek under the hood of Unity.

Create a new scene.
Create an empty GameObject and position it at 0,0,0.  Rename it to Maze Generator
Attach the following script to it:



using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Maze : MonoBehaviour {

public Transform playerPrefab;

internal Vector3 mazeOrigin = Vector3.zero;

public static int MazeWidth = 10; //cubes across
public static int MazeHeight = 10;//cubes down

public static int ChanceofWall = 25;

public int PlayerStartX = 1;
public int PlayerStartY = 1;

public static Texture WallMaterial;
public static Texture FloorMaterial;

public int[,] MazeLayout;

internal List<GameObject> cubes;


void Awake()
{
MazeLayout = new int[MazeWidth, MazeHeight];
cubes = new List<GameObject>(); 
}

void Start () 
{
this.transform.position = mazeOrigin;

MakeFloor();
MakeBoundaries();
MakeLayout();

PlacePlayer();
}

void MakeFloor()
{
for(int x = 0; x < MazeLayout.GetLength(0); x++)
{
for(int y = 0; y < MazeLayout.GetLength(1); y++)
{
GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
Vector3 pos = new Vector3(mazeOrigin.x + x, -1f, mazeOrigin.y + y);

cube.transform.position = pos;
cube.tag = "Floor";
cube.renderer.material.mainTexture = FloorMaterial;
cube.transform.parent = this.transform;
cubes.Add(cube);
}
}
}

void MakeBoundaries()
{
for(int x = 0; x < MazeLayout.GetLength(0); x++)
{
for(int y = 0; y < MazeLayout.GetLength(1); y++)
{
if(x==0 || y == 0 || x == MazeWidth - 1 || y == MazeHeight - 1)
{
GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
Vector3 pos = new Vector3(mazeOrigin.x + x, 0, mazeOrigin.y + y);

cube.transform.position = pos;
cube.tag = "Wall";
cube.renderer.material.mainTexture = WallMaterial;
cube.transform.parent = this.transform;
cubes.Add(cube);
}
}
}
}

void MakeLayout()
{
for(int x = 0; x < MazeLayout.GetLength(0); x++)
{
for(int y = 0; y < MazeLayout.GetLength(1); y++)
{
if(x > 1 && y > 1 && x < MazeWidth -2 && y < MazeHeight -2)
{
int wall = Random.Range(0, 100);
if(wall<ChanceofWall)
{
GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
Vector3 pos = new Vector3(mazeOrigin.x + x, 0, mazeOrigin.y + y);

cube.transform.position = pos;
cube.tag = "Wall";
cube.renderer.material.mainTexture = WallMaterial;
cube.transform.parent = this.transform;
cubes.Add(cube);
}
}
}
}
}

void PlacePlayer()
{
Instantiate(playerPrefab, new Vector3(PlayerStartX, 0.1f, PlayerStartY), Quaternion.identity);
}
}

I made a player prefab out of a cylinder.  I also moved the camera directly above the cylinder, pointing directly downwards and made it a child of the player object.  This will cause it to always follow the player.  Assign the player prefab to the Maze script in the inspector.  Add a character controller component.  Attach the following script to the Player object (contains controls for keyboard and xbox gamepad movement)

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(AudioSource))]
public class Controller : MonoBehaviour {


public float MovementSpeed = 6.0f;
public float RotationSpeed = 100f;
public float JumpSpeed = 16.0f;
public float Gravity = 20.0f;

private Vector3 moveDirection = Vector3.zero;

void FixedUpdate ()
{
checkMovement();
}

void checkMovement()
{
CharacterController controller = GetComponent<CharacterController>();


if (controller.isGrounded)
{
moveDirection = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
moveDirection = transform.TransformDirection(moveDirection);
moveDirection *= MovementSpeed;

if (Input.GetButton("Jump"))
{
                                moveDirection.y = JumpSpeed;
}
}
moveDirection.y -= Gravity * Time.deltaTime;
controller.Move(moveDirection * Time.deltaTime);
}
}



At this point, if you run it, you will see a 10x10 maze with white textures.  I made WallMaterial and FloorMaterial static members because I needed to access them from a setup menu I made.  If you do not intend to also create the setup scene then you will need to assign textures to those members yourself.  If you remove the static prefix they will show up in the inspector.  The setup screen will allow you to adjust the height and width of the generated maze, adjust the chance that a wall will appear, and select the textures to use on the walls and floor.

Creating the setup screen.

Create a folder in the root of your project called Resources.  Drag each texture into this folder.


Create a new scene
Create an empty game object, call it GUIObject.
Attach the following script to it:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class GUIScript : MonoBehaviour {
public Texture2D icon;
private List<Texture> walltextures;
private List<Texture> floortextures;
private List<string> wallnames;
private List<string> floornames;
void Start () {
walltextures = new List<Texture>();
floortextures = new List<Texture>();
wallnames = new List<string>();
floornames = new List<string>();
wallnames.Add("Crate01");
wallnames.Add("Crate01b");
wallnames.Add("Crate02");
wallnames.Add("Crate02b");
wallnames.Add("Crate03");
wallnames.Add("Crate04");
wallnames.Add("Wall01");
wallnames.Add("Wall02");
wallnames.Add("Rock01");
wallnames.Add("ScifiPanel03");
floornames.Add("Floor01");
floornames.Add("Floor02");
floornames.Add("Floor03");
floornames.Add("Floor04");
floornames.Add("MetalBasic01");
floornames.Add("Rock02");
floornames.Add("Rock03");
floornames.Add("Rock04");
floornames.Add("ScifiPanel01");
floornames.Add("ScifiPanel02");
loadTextures();
}
void OnGUI()
{
//Make Background Box
GUI.Box (new Rect(10,10,670,300), "Maze Setup");
#region Height and Width
GUI.Label(new Rect(15,35,80, 20), "Maze Width");
if(GUI.Button(new Rect(105, 35, 20, 20), "-"))
{
MazeSettings.MazeWidth--;
}
if(GUI.Button(new Rect(125, 35, 20, 20), "+"))
{
MazeSettings.MazeWidth++;
}
GUI.Label(new Rect(150, 35, 30,20), MazeSettings.MazeWidth.ToString());
GUI.Label(new Rect(15,55,80, 20), "Maze Height");
if(GUI.Button(new Rect(105, 55, 20, 20), "-"))
{
MazeSettings.MazeHeight--;
}
if(GUI.Button(new Rect(125, 55, 20, 20), "+"))
{
MazeSettings.MazeHeight++;
}
GUI.Label(new Rect(150, 55, 30, 20), MazeSettings.MazeHeight.ToString());
#endregion
#region Randoms
GUI.Label(new Rect(15, 75, 110, 20), "Chance of Wall");
if(GUI.Button(new Rect(105, 75, 20, 20), "-"))
{
MazeSettings.ChanceofWall--;
}
if(GUI.Button(new Rect(125, 75, 20, 20), "+"))
{
MazeSettings.ChanceofWall++;
}
GUI.Label(new Rect(150, 75, 100, 20), MazeSettings.ChanceofWall.ToString() + "%");
#endregion
#region Textures
GUI.Label(new Rect(15, 95, 100, 20), "Wall Texture");
if(GUI.Button(new Rect(15, 115, 64, 64), walltextures[0]))
{
MazeSettings.WallMaterial = walltextures[0];
}
if(GUI.Button(new Rect(15 + 64, 115, 64, 64), walltextures[1]))
{
MazeSettings.WallMaterial = walltextures[1];
}
if(GUI.Button(new Rect(15 + 64 * 2, 115, 64, 64), walltextures[2]))
{
MazeSettings.WallMaterial = walltextures[2];
}
if(GUI.Button(new Rect(15 + 64 * 3, 115, 64, 64), walltextures[3]))
{
MazeSettings.WallMaterial = walltextures[3];
}
if(GUI.Button(new Rect(15 + 64 * 4, 115, 64, 64), walltextures[4]))
{
MazeSettings.WallMaterial = walltextures[4];
}
if(GUI.Button(new Rect(15 + 64 * 5, 115, 64, 64), walltextures[5]))
{
MazeSettings.WallMaterial = walltextures[5];
}
if(GUI.Button(new Rect(15 + 64 * 6, 115, 64, 64), walltextures[6]))
{
MazeSettings.WallMaterial = walltextures[6];
}
if(GUI.Button(new Rect(15 + 64 * 7, 115, 64, 64), walltextures[7]))
{
MazeSettings.WallMaterial = walltextures[7];
}
if(GUI.Button(new Rect(15 + 64 * 8, 115, 64, 64), walltextures[8]))
{
MazeSettings.WallMaterial = walltextures[8];
}
if(GUI.Button(new Rect(15 + 64 * 9, 115, 64, 64), walltextures[9]))
{
MazeSettings.WallMaterial = walltextures[9];
}
GUI.Label(new Rect(15, 95 + 84, 100, 20), "Floor Texture");
if(GUI.Button(new Rect(15, 199, 64, 64), floortextures[0]))
{
MazeSettings.FloorMaterial = floortextures[0];
}
if(GUI.Button(new Rect(15 + 64, 199, 64, 64), floortextures[1]))
{
MazeSettings.FloorMaterial = floortextures[1];
}
if(GUI.Button(new Rect(15 + 64 * 2, 199, 64, 64), floortextures[2]))
{
MazeSettings.FloorMaterial = floortextures[2];
}
if(GUI.Button(new Rect(15 + 64 * 3, 199, 64, 64), floortextures[3]))
{
MazeSettings.FloorMaterial = floortextures[3];
}
if(GUI.Button(new Rect(15 + 64 * 4, 199, 64, 64), floortextures[4]))
{
MazeSettings.FloorMaterial = floortextures[4];
}
if(GUI.Button(new Rect(15 + 64 * 5, 199, 64, 64), floortextures[5]))
{
MazeSettings.FloorMaterial = floortextures[5];
}
if(GUI.Button(new Rect(15 + 64 * 6, 199, 64, 64), floortextures[6]))
{
MazeSettings.FloorMaterial = floortextures[6];
}
if(GUI.Button(new Rect(15 + 64 * 7, 199, 64, 64), floortextures[7]))
{
MazeSettings.FloorMaterial = floortextures[7];
}
if(GUI.Button(new Rect(15 + 64 * 8, 199, 64, 64), floortextures[8]))
{
MazeSettings.FloorMaterial = floortextures[8];
}
if(GUI.Button(new Rect(15 + 64 * 9, 199, 64, 64), floortextures[9]))
{
MazeSettings.FloorMaterial = floortextures[9];
}
#endregion
if(GUI.Button(new Rect(15, 275, 110, 20), "Generate Maze"))
{
Application.LoadLevel("maze");
}
}
void loadTextures()
{
foreach(string name in floornames)
{
floortextures.Add(Resources.Load(name) as Texture);
}
foreach(string name in wallnames)
{
walltextures.Add(Resources.Load(name) as Texture);
}
}
}


The string being entered into wallnames and floornames must match the names in the resources folder.  The resources folder is required to load assets on the fly.  You will need to adjust the line near the end 'Application.LoadLevel("maze");'.  Replace maze with the name of the scene you just saved.   Set this setup scene as the default scene.

Setup Screen

Game View.  Notice the camera is position above the player and looking straight down.

30x30 maze seen from scene view

150x150 maze seen from scene view





Tuesday, May 14, 2013

Defining ContextMenus for ListView Items in Xamarin Android

While working on my Android app I had need to display a list of items with position-dependent context menus.  It took me awhile to develop a clean solution so I'd like to share my work with anyone who would like dynamic menus available on their ListViews that display over items using a long-press.

Inside of my Activity class I have declared three important variables:

private ListView itemList;
private int selectedItemNumber;
private string selectedItemText;

What each variable tracks should be self explanatory from the declaration, but please comment if you have questions on anything in my posts.  The reason it's important to declare these within scope of the Activity class and not within a method is because we lose focus of what ListView item is being interacted with in-between the step of creating a menu and when that menu is used.  There might be a way to extract that information from the second step, but none was obvious to me and I found the approach of using private variables an easy solution.

In addition to those three variables I've also created constant values to track the int-based menu options:

private const int EDIT_ITEM = 0;
private const int VIEW_ITEM = 1;
private const int DELETE_ITEM = 2;

These allow me to refer to actions in a clear way in the code.  In the OnCreate method of my Activity there are two important steps needed to use context menus:

protected override void OnCreate (Bundle bundle)
{
 base.OnCreate (bundle);
 SetContentView (Resource.Layout.SendTickets);

 //Two important steps to for my context menus
 itemList = (ListView) FindViewById(Resource.Id.myListViewName);
 RegisterForContextMenu(itemList);

}

Now that I've associated my ListView with a private variable and have registered it for ContextMenus I only have to define two additional methods for everything to work.  The first is the method that is triggered when an item is selected because of the RegisterForContextMenu call made in OnCreate, and it creates the menu that is displayed:

public override void OnCreateContextMenu(IContextMenu menu, View v, IContextMenuContextMenuInfo info)
{
 AdapterViewAdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo) info;
 
 selectedItemText = ((TextView) menuInfo.TargetView).Text;
 selectedItemId = menuInfo.Id;

 //My listview is set to have a "No Data" item on the first row if it's empty
 if (!selectedTicketText.Equals("No Data"))
 {
  menu.SetHeaderTitle("My Menu Header");
  //All items can be viewed
  menu.Add(0, VIEW_ITEM, 0, "View Item");
  //But only the first item in the list can be edited
  if (selectedItemId== 0)
   menu.Add(0, EDIT_ITEM, 0, "Edit Item");

  if (!selectedTicketText.Contains ("Words that mean it can be deleted"))
   menu.Add (0, DELETE_ITEM, 0, "Delete Item");
 }
 else  //This won't create any menu at all
 {
  menu.SetHeaderTitle("No Options");
 }
}

Okay, so now my menu will show up and it will look different based on what text the item in the ListView contains, and what item number it is assigned, which is the 0-based index in the listview.  You can see in my menu code above that I have added the ability to View to any-and-all items, but only the first item in the list can be edited, and only items containing my special text can be deleted.

The last step is to specify what actions occur when the menu is used.  We do this in the OnContextItemSelected method:

public override bool OnContextItemSelected(IMenuItem item)
{
 if (item.ItemId.Equals (VIEW_ITEM)) 
 {
  MethodThatViewsMyItem(selectedItemText);
 } 
 else if (item.ItemId.Equals(EDIT_ITEM))
 {
  MethodThatEditsMyItem(selectedItemNumber);
 }
 else if (item.ItemId.Equals(DELETE_ITEM))
 {
  MethodThatDeletesMyItem(selectedItemNumber);
 }

 return base.OnOptionsItemSelected(item);
}

This is where the information captured as selectedItemText and selectedItemNumber from OnCreateContextMenu comes in handy.  Since this method only has access to the item that was clicked, and that IMenuItem contains no information about the ListView item we have to refer to our Activity-scope variables selectedItemText and selectedItemNumber.

As always, feel free to post questions or comments, even if it's to suggest a better way of doing this!