When you develop an app, you probably have a few parameters that you need to adjust and tweak to find the desired value. The usual approach would be to change the parameter in the source code, recompile and deploy the app to your phone to test it. If you have a lot of parameters to tweak, you probably add a developer GUI to your app that enables you to change the parameters at runtime.
In this blog post I present an easy to integrate alternative. Instead of reading the values from your source code or an in game GUI, the parameter values are stored and retrieved in a cloud database. The technology is build onto my former blog post about using Amazon SimpleDB, hence for this we use SimpleDB again. The idea of this approach is like this:
Store all parameters you want to edit in an own class, e.g.
AppParameter. When running your app in
edit mode you will retrieve the values for
AppParameter from the database. On your PC you run a .NET application that allows you to edit the database parameters. Reread the values inside your app and test the changed parameters. No need to recompile or even restart you app and edit the parameters from any PC.
To make the integration easy I present a solution that uses C# reflections to automatically determine the fields of the
AppParameter class. Saving and reading the parameter class to and from the SimpleDB uses generics. That means you can use the code for any class you define. Also the PC-Editor automatically creates the GUI for the class parameters. This is a picture of the Editor using some arbitrary game parameters:
The parameter class used here:
public class AppParamter
{
public float JumpForce;
public float Gravity;
public float Height;
public float Weight;
public Vector2 StartPosition;
public Vector2 LevelOffset;
public int NumOfLives;
public int MaxItems;
public int MinItems;
}
If you want to use this with your app you only have to change the
AppParameter class to contain the parameters you need. However, I currently only support the data format
float,
Vector2 and
int. If you need other formats you need to extend the example.
The Editor provides buttons to read and write the values from and to the Amazon SimpleDB. With the
Clear DB button you can delete the complete entry for this parameter class. This is recommendable every time you change the structure of the parameter class.
Copy Values creates a code snippet and copies it to the clipboard that sets the current values of your class. Use this once you finalized your parameters to set the values inside your app without the database. The code snippet created for this example is:
public void SetValues()
{
JumpForce = 2.3f;
Gravity = 7f;
Height = 12f;
Weight = 55f;
StartPosition = new Vector2(12f, 25f);
LevelOffset = new Vector2(-3f, -2f);
NumOfLives = 7;
MaxItems = 5;
MinItems = 1;
}
Implementation
The core class of this project is the
ParameterProcessor. It is realized as a singleton and provides the interface to the database. Furthermore it provides functions to create strings from values and the other way round. You will find a lot of similarities to my previous SimpleDB blog post.
The class is designed to be used from the PC-Editor as well as from the phone app. Therefore I did not use the Amazon AWS SDK for .NET for the SimpleDB access from Windows, but used the SDK designed for the Windows Phone. For that I needed to create Windows Library Project and add the SDK source files. Compiling and running it from Windows worked smooth with only one exception. In
SimpleDBResponsce.cs line 39, the
IsRequestSuccessful property for set is declared protected. This caused the
XmlSerializer to throw an Exception. Removing the protected modifier solved the problem. Now let us take a look at the code. In the beginning the singleton is defined and in the constructor the amazon client is initialized. Don't forget to enter your SimpleDB keys there.
public class ParameterProcessor
{
#region singleton
// create singleton
static readonly ParameterProcessor instance = new ParameterProcessor();
/// <summary>
/// Get instance of singleton.
/// </summary>
public static ParameterProcessor Instance
{
get
{
return instance;
}
}
/// <summary>
/// Explicit static constructor to tell C# compiler
/// not to mark type as beforefieldinit
/// </summary>
static ParameterProcessor()
{
}
#endregion
// db
AmazonSimpleDB m_sdb;
/// <summary>
/// Constructor
/// </summary>
ParameterProcessor()
{
// intitialize db
// amazon cloud access
m_sdb = AWSClientFactory.CreateAmazonSimpleDBClient("YourPublicKey", "YourPrivatKey");
}
The function SaveToDB uses generics to pass the parameter class and instance of which values are stored in the database. The second function parameter is the name of the domain in SimpleDB. The domain needs to be created before. The response handler, which is called after the parameters have been stored in the database, currently does nothing. Using C# reflections the fields of the parameter class are easily iterated through an
FieldInfo array provided by the function G
etFields() from the
Type class. The database then simply stores the field name as an attribute with the field value converted to a string. Additionally I save a time stamp that could be used to check if data has been updated without retrieving all attribute. This could make sense if the data you store is very huge. The complete class values are entered as one item into SimpleDB where the parameter class name is used as key value for the item. By that you could store multiple different parameter classes within one SimpleDB domain.
/// <summary>
/// Takes a class and enters its fields to an Amazon SimpleDB
/// </summary>
/// <typeparam name="T">Serializable Class to be saved into the database</typeparam>
/// <param name="data">Class instance that is saved into the database</param>
/// <param name="tableName">Name of domain in SimpleDB</param>
public void SaveToDB<T>(T data, string tableName)
{
SimpleDBResponseEventHandler<object, ResponseEventArgs> handler = null;
handler = delegate(object senderAmazon, ResponseEventArgs args)
{
//Unhook from event.
m_sdb.OnSimpleDBResponse -= handler;
PutAttributesResponse response = args.Response as PutAttributesResponse;
if (null != response)
{
}
else
{
}
};
m_sdb.OnSimpleDBResponse += handler;
string structName = data.GetType().Name;
PutAttributesRequest putAttributesRequest = new PutAttributesRequest { DomainName = tableName, ItemName = structName };
List<ReplaceableAttribute> attributesOne = putAttributesRequest.Attribute;
Type type = data.GetType();
System.Reflection.FieldInfo[] fields = type.GetFields();
foreach (System.Reflection.FieldInfo field in fields)
{
string valueString = FieldToString(field, data);
attributesOne.Add(new ReplaceableAttribute().WithName(field.Name).WithValue(valueString).WithReplace(true));
}
attributesOne.Add(new ReplaceableAttribute().WithName("TimeStamp").WithValue(DateTime.Now.Ticks.ToString()).WithReplace(true));
m_sdb.PutAttributes(putAttributesRequest);
}
The
ReadFromDB function also uses generics and reads the values from the database into the passed parameter class instance. Additionally you can pass an
Action delegate, that is called after the data is received. Remember that the database calls are asynchronous, so if you for example need to refresh your GUI after the new values are received, you can use this delegate.
The select request retrieves the SimpleDB item with the key value (itemName) of the parameter class name. The request handler then iterates through the retrieved attributes and using reflections sets the values into the fields of the parameter class instance.
/// <summary>
/// Reads values from a SimpleDB and writes it into a class instance.
/// </summary>
/// <typeparam name="T">Serializable Class that is read from the database</typeparam>
/// <param name="data">Class instance where the database values are written into</param>
/// <param name="tableName">Name of domain in SimpleDB</param>
/// <param name="readFinished">A delegate that is called when reading the values has finished. Set null if not needed.</param>
public void ReadFromDB<T>(T data, string tableName, Action readFinished)
{
// create response handler and delegate
SimpleDBResponseEventHandler<object, ResponseEventArgs> handler = null;
handler = delegate(object sender, ResponseEventArgs args)
{
//Unhook from event.
m_sdb.OnSimpleDBResponse -= handler;
SelectResponse response = args.Response as SelectResponse;
if (null != response)
{
Type type = data.GetType();
System.Reflection.FieldInfo[] fields = type.GetFields();
SelectResult selectResult = response.SelectResult;
if (null != selectResult)
{
foreach (Item item in selectResult.Item)
{
// actually should just be one item
foreach (Amazon.SimpleDB.Model.Attribute attribute in item.Attribute)
{
var field = fields.FirstOrDefault(f => f.Name == attribute.Name);
if (field != null)
{
StringToFieldValue(field, data, attribute.Value);
}
}
}
}
if (readFinished != null)
{
readFinished.Invoke();
}
}
};
m_sdb.OnSimpleDBResponse += handler;
string structName = data.GetType().Name;
// create request
string sql = "SELECT * FROM " + tableName + " WHERE itemName()='" + structName + "'";
m_sdb.Select(new SelectRequest { SelectExpression = sql, ConsistentRead = true });
}
SimpleDB can only store string values, therefore the parameter class field have to be converted to a string. This is done by the
FieldToString function. The current implementation explicitly only supports float, int and Vector2. You can add more types if you need them here.
/// <summary>
/// Using reflection to get a value string of a class field.
/// </summary>
/// <param name="field">The field of the class to get the value from</param>
/// <param name="data">Class instance with values</param>
/// <returns></returns>
public String FieldToString(System.Reflection.FieldInfo field, object data)
{
string valueString;
if (field.FieldType == typeof(float))
{
valueString = ((float)field.GetValue(data)).ToString(System.Globalization.CultureInfo.InvariantCulture.NumberFormat);
}
else if (field.FieldType == typeof(Vector2))
{
Vector2 vec = (Vector2)field.GetValue(data);
System.Globalization.NumberFormatInfo nfi
= System.Globalization.CultureInfo.InvariantCulture.NumberFormat;
valueString = vec.X.ToString(nfi) + ", "
+ vec.Y.ToString(nfi);
}
else
{
// just use the default ToString for all other field types
// extend it if you need special handling like the types before
valueString = field.GetValue(data).ToString();
}
return valueString;
}
The complement function to the above takes a string and writes the value into the field of the passed instance. Again only float, int and Vector2 is supported here.
/// <summary>
/// Using reflection to set a class field from a value string.
/// Currently only float, Vector2 and int is supported.
/// Extend for you needs.
/// </summary>
/// <param name="field">The field of the class for which the value will be set.</param>
/// <param name="data">Class instance where the value will be stored into</param>
/// <param name="valueString">A string containing a value for the field</param>
public void StringToFieldValue(System.Reflection.FieldInfo field, object data, string valueString)
{
if (field.FieldType == typeof(float))
{
field.SetValue(data, float.Parse(valueString, System.Globalization.CultureInfo.InvariantCulture.NumberFormat));
}
else if (field.FieldType == typeof(Vector2))
{
System.Globalization.NumberFormatInfo nfi
= System.Globalization.CultureInfo.InvariantCulture.NumberFormat;
String[] values = valueString.Split(',');
Vector2 point = new Vector2();
try
{
point.X = float.Parse(values[0], nfi);
point.Y = float.Parse(values[1], nfi);
}
catch
{
// oh no
}
field.SetValue(data, point);
}
if (field.FieldType == typeof(int))
{
field.SetValue(data, Int32.Parse(valueString, System.Globalization.CultureInfo.InvariantCulture.NumberFormat));
}
else
{
// unsupported type
}
}
The Windows
DB Parameter Editor uses the above class to read and write to and from the database. I will not go much into the details of the editor source code here. Worth mentioning is the dynamic creation of the parameter GUI. Therefore exists a base class
ParameterControl, which is derived from
UserControl. This base class also has a static function
CreateParameterControl, which serves as a factory for the actual derived parameter controls. According to the type of the passed
FieldInfo different controls can be generated. Anyhow, in this example I only implemented the TextBoxParameterControl, where you enter the values into a simple text box. Possible extensions could for example be a slider control for floats, a checkbox control for a bool parameter or a color picker control for a color field. Inside the main form (
Form1.cs) the controls are then created like this:
Type type = classData.GetType();
System.Reflection.FieldInfo[] fields = type.GetFields();
int offset = 15;
foreach (System.Reflection.FieldInfo field in fields)
{
ParameterControl control = ParameterControl.CreateParameterControl(field, classData);
control.Top = offset;
control.Left = 5;
offset += control.Height;
this.parameterGroupBox.Controls.Add(control);
}
Using the
ParameterProcessor class you can finally integrate the database access into your application. Just reread the parameter class values at your convenience. You could add an update button, or reread every time you restart a level or even poll at a certain time interval.
Download and Conclusion
The complete source code of the
DB Parameter Editor can be downloaded from my homepage:
DBParameterEditor.zip
It is developed using Microsoft Visual Studio 2010 Express, but it should not be a problem to use it in MonoDevelop. I also included the AWSSDKWP7 (SDK for SimpleDB) as Windows binary, so that you can directly start testing.
In this post I presented an easy way to tweak app parameters from a PC using a cloud database. This scenario can be extended in multiple directions. For example you could extend the Editor to become a more flexible game level editor. By that your game and level designers can modify levels on the running application. You could also extend it to be usable with a release version of a game. By that you can make game play adjustments without the need of an update.