Game Development Community

dev|Pro Game Development Curriculum

Using Scripts with XNA

by Johnathon · 11/18/2009 (1:51 am) · 0 comments

Today I recieved an interesting note from Diego saying that a video demonstration would be nice, showing how my Managed Scripting engine works with XNA projects and explain in a little more detail how the engine works with XNA, and possibly with Torque X if TorqueComponent's can be registered/added to a Components list during runtime like XNA components can. I'm not sure as I don't own Torque X at the moment.

There is two different demonstrations I want to showcase, both showing the engine compiling the scripts at runtime and allowing for the scripts to be re-compiled while the game is still running. Each with source code, and a video demonstrating it in action.

Example 1

Adding Script support within your game
We will first create a new XNA project, using Microsofts XNA Game template.
farm3.static.flickr.com/2637/4113517573_3ca55c086b_o.jpg
Next we will need to add a reference to the Managed Scripting XNA Engine, please note that the XNA library shown in this picture is an XNA specific version of the library. The standard Managed Scripting for Windows engine that is currently available for download will work with this example as well. Once you add a reference to the engine, add another reference, this time to the .NET assembly System.Windows.Forms.dll
farm3.static.flickr.com/2568/4114290786_934d0f814c_o.jpg
Now that we have the project created we will need to modify the Game.cs file to call a new method within it's Initialize method called CompileScripts()

Here we are creating our method, instancing a new copy of the engine, setting the compiler to use the ScriptCompiler and telling the engine we want to compile the objects into memory rather than a local .dll file
private void CompileScripts()
        {
            engine = new ScriptingEngine();
            engine.Compiler = CompilerSelections.ScriptCompiler;
            engine.CompileStyle = ScriptCompileStyle.CompileToMemory;

Next we need to add references to the XNA framework, this is accomplished using engine.AddReference and is required in order for our scripts to inherit from GameComponent or DrawableGameComponent
engine.AddReference(@"C:Program Files/Microsoft XNA/XNA Game Studio/v3.0/References/Windowsx86/Microsoft.Xna.Framework.dll");
            engine.AddReference(@"C:Program Files/Microsoft XNA/XNA Game Studio/v3.0/References/Windowsx86/Microsoft.Xna.Framework.Game.dll");

Next let's create an array containing a list of scripts found within our content/scripts folder. We check to see if the array is empty and if so, quit the method, as there is nothing to compile.
string[] files = System.IO.Directory.GetFiles("Content/Scripts", "*.cs");
            if (files.Length == 0)
            {
                return;
            }

Now that we have a collection of scripts to compile, we need to send them to the compiler for compiling. The compiler returns a string with the results of the compilation (success or failure) and we store that within our Results string. Once compiling is done we check for errors. If the string isn't the standard 'compiled without errors' string, then we know the compilation failed, and we want to display a messagebox showing why.
string results = engine.Compile(files);

            if (results != "Compiled without errors.")
            {
                MessageBox.Show(results);
                return;
            }

Now we get to create and use our scripts, the engine holds a copy of the assembly we just compiled the scripts into, and it's holding that copy in memory since we told it too back at the beginning of the method. Since it holds the assembly, we can get every Type (compiled script) that it currently contains.
Type[] types = engine.GetAssembly.GetTypes();

With an array of Types now collected, we can sift through them all and find the ones that inherit from GameComponent, and only use those.
foreach (Type t in types)
            {
                if (t.BaseType.Name == "GameComponent")
                {

Once we have found a script that inherits from GameComponent, we will tell the engine to create an instance of that script. The engine accepts an array of arguments when instancing objects, and so we will pass 'this' which is the current Game. This allows the scripts to access the Game and access its components list and other Game setup options.
engine.InstanceObject(t, new object[] { this });

Finally we will create a Script Object. A Script Object holds an instance of the script, and allows various options for manipulating the script. Script Objects allow you to Access and/or modify scripts properties, and invoke their methods. I will put together another example over the next couple of days showing accessing the scripts methods/functions and its properties during runtime.
ManagedScripting.ScriptObject obj = engine.GetObject(t.Name);
                    GameComponent gc = (GameComponent)obj.Instance;
                    this.Components.Add(gc);
                }
            }
        }

Ok, now that we have the source code demonstrated, here is the video showing it in action. Note that the button in the video just calls 'CompileScripts' again, allowing for the scripts to be re-compiled while the game is still running. Developers can be more precise in how script recompiling is handled, allowing for the engine to recompile the scripts, but keep all objects in the games Components list except that of the changed scripts. Developers can decide how they want their games logic to handle script recompiling.


Example 2

Easy Scripting
Managed Scripting helps you piece together classes if you need to, and while the following example is more of a hack way of getting it done since the ClassGenerator class is not fully completed yet, it does work. A future update will complete the class and allow for better code building support. When the sample is over, we will be able to write scripts using properties and methods that are not contained within a class or namespace. The class name and namespace will be assigned to each script during its runtime compiling.

We will create another method called CompileShortScripts() and just like in our first example, we will create a new instance of the engine, and setup some initial engine settings, like the compiler to use and the references to add.
public void CompileShortScripts()
        {
            engine = new ScriptingEngine();
            engine.Compiler = CompilerSelections.SourceCompiler;
            engine.CompileStyle = ScriptCompileStyle.CompileToMemory;
            engine.AddReference(@"C:Program Files/Microsoft XNA/XNA Game Studio/v3.0/References/Windowsx86/Microsoft.Xna.Framework.dll");
            engine.AddReference(@"C:Program Files/Microsoft XNA/XNA Game Studio/v3.0/References/Windowsx86/Microsoft.Xna.Framework.Game.dll");

Next we will use the engines CodeBuilding classes to create a new class.
ManagedScripting.CodeBuilding.ClassGenerator newClass = new ManagedScripting.CodeBuilding.ClassGenerator();
            newClass.ClassName = "Startup";
            newClass.ComponentType = ManagedScripting.CodeBuilding.ClassGenerator.ComponentTypes.GameComponent;
            newClass.Modifier = ManagedScripting.CodeBuilding.ClassGenerator.Modifiers.Public;
            newClass.Namespace = "Example";

Now that we have our class setup, lets get a collection of all our scripts needing to be compiled.

string[] codeFiles = System.IO.Directory.GetFiles("ContentScripts", "*.cs");

With our script collection available now, we need to loop through each script. Since the ClassGenerator was primarily built to use MethodSetup classes to build methods for it, we need to do a hacky way of injecting a method into the class until I can fix the ClassGenerator.InsertCode not working properly. It returns the added code instead of inserting the code into the ClassGenerator.Code property like it should.
The ClassGenerator.InsertCode() method doesn't accept an array, and the System.IO.File.ReadAllLines method returns an array of strings containing each line of the file it reads in. Thus forcing us to build a new string by looping through each string in the array.
foreach (string codeFile in codeFiles)
            {
                string[] code = System.IO.File.ReadAllLines(codeFile);
                string final = "";
                foreach (string line in code)
                {
                    final += line + "n";
                }

With the scripts code stored in our 'final' variable now, we need to add it to our newClass using the InsertCode method, which returns our scripts code, incapsulated within a class and namespace now. We then just write the string out to a temp file so we can compile the entire class.
string temp = newClass.InsertCode(final);
                string filename = System.IO.Path.GetFileName(codeFile) + ".comp";
                System.IO.StreamWriter sw = new System.IO.StreamWriter("ContentScripts" + filename);
                sw.Write(temp);
                sw.Close();
            }

Lets check to make sure we have converted scripts available to compile.
string[] files = System.IO.Directory.GetFiles("ContentScripts", "*.comp");
            if (files.Length == 0)
            {
                IsCompiling = true;
                return;
            }

Now we can go ahead and compile them. Once compiling is done, we delete the temporary script files we generated and check for compiler errors.
string results = engine.Compile(files);

            foreach (string file in files)
                System.IO.File.Delete(file);

            if (results != "Compiled without errors.")
            {
                MessageBox.Show(results);
                return;

With compiling done, we just perform the same loop we did in the first example, scanning through the assemblies Types and adding the GameComponents to the games component list. This however is why the XNA version of the engine is going to be required in order to use the short script style of scripting for your project as the ClassGenerator will add the needed code when it is creating our temporary class in order for our scripts to inherit from an XNA class. If you used the standard windows version of the engine, then this isn't going to work, as you won't have any classes that inherit from GameComponent.
Type[] types = engine.GetAssembly.GetTypes();

            foreach (Type t in types)
            {
                if (t.BaseType.Name == "GameComponent")
                {
                    engine.InstanceObject(t, new object[] { this });

                    ManagedScripting.ScriptObject obj = engine.GetObject(t.Name);
                    GameComponent gc = (GameComponent)obj.Instance;
                    this.Components.Add(gc);
                }
            }

And now that we have our compiled code, you can create a script called 'test.cs' and place the following code inside of it, and run your game. The compiler will compile it and execute the Initialize() method during your games startup (provided you CompileScripts() call is within your Game.Initialize() method)
public override void Initialize()
{
	this._Game.Window.Title = "An even simpler script!";
}

Finally the video demonstrating example 2 in action.

Please note that Managed Scripting for XNA is available on my CodePlex site for GarageGame developers to play with if they want until I can get a complete version finished and released on my official site with documentation. It is not available on the official site for download yet, as I want to flesh out several things prior to making it public. I wanted the GG community to see it in action though, and hear some feedback on it. If you want to use standard scripts as shown in the first example you can use the Beta 1 version for Windows which includes documentation and it will run just fine.

Lastly, since I don't own a license to Torque X and my demo has expired, I have no way of messing with the engine and Torque X to see how they work together. If anyone here tries it I'd appreciate hearing some feedback if it works well with Torque X or if it doesn't work at all. If I can get enough interested generated in the engine (both within and outside of the GG community), then I will purchase a license and work on getting a Torque X only copy built if the standard XNA version won't cut it.

As always, thanks for any feedback.