Game Scripting in IronPython

by calbert 9/16/2008 6:20:14 PM

Since my last post about game scripting I have had the chance to play around more with IronPython. I wrote a quick MUD type demo game that utilizes C# and IronPython on the server and uses Silverlight 2 as the user interface.

Play Cameron's Dungeon!

I wanted to explore this scripting for Perenthia because I want to be able to add dynamic functions or scripts to objects. I wrote this sample to test out using C# as base classes for IronPython classes and passing operations back and forth between the two. As such the source is kind of loose but the concepts are demonstrated.

What I have is a basic framework of C# classes that define an Actor (any object in the game), a Client (a connected player), a Game (the logic of the game) and a Server (for handling HTTP communication, etc.). The Actor, Client and Game classes also server as the base classes for IronPython classes or Creatures, NPCs, Players and Rooms.

Server is the main C# class and gets initialized in the global.asax file like so:

protected void Application_Start(object sender, EventArgs e)
        {
            SLGameEngine.Server.Start(Server.MapPath("python/startup.py"),
                new string[] 
                { 
                    Server.MapPath("python"), 
                    Server.MapPath("bin"),
                    System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory()
                });
        }

Inside the Server class a static class called PyEngine handles all the execution of the IronPython code. The Start method inside the server class makes a few calls into the PyEngine that starts the engine and executes the code in the /python/startup.py script:

public static void Start(string startupFilePath, string[] searchPaths)
        {
            try
            {
                PyEngine.Startup(searchPaths);
                PyEngine.AddAssembly(typeof(Game).Assembly);

                PyEngine.ExecuteFile(startupFilePath);

                _timer = new Timer(300000);
                _timer.Elapsed += new ElapsedEventHandler(_timer_Elapsed);
                _timer.Start();
            }
            catch (Exception ex)
            {
                Log.Write(ex.ToString());
            }
        }

The first two lines initialize the scripting engine while the third actually executes the startup.py script. Inside the startup script is called to create a global instance of the Game class and to start the game running. The timer stuff is used to remove inactive players. After the game is started any input sent from the user to the server is handled by an IHttpHandler class called CommandHandler. This class parses the request and send the input to the Server class that will actually pass the handling of the input to the IronPython Game class.

public void ProcessRequest(HttpContext context)
        {
            string response = "Invalid Command";
            try
            {
                string cmd = context.Request.Form["cmd"];
                string ip = context.Request.UserHostAddress;
                if (!String.IsNullOrEmpty(cmd))
                {
                    if (cmd.StartsWith("NEW"))
                    {
                        response = Intro.GetIntro(context);
                    }
                    else if (cmd.StartsWith("NAME "))
                    {
                        string name = cmd.Substring(5);
                        if (Server.NameExists(name))
                        {
                            response = "That name has already been used, please choose another.";
                        }
                        else
                        {
                            Server.AddClient(ip, name);
                            response = String.Format("Welcome {0}!{1}{2}", name, Environment.NewLine, Server.GetClientOutput(ip));
                        }
                    }
                    else
                    {
                        Server.ProcessInput(ip, cmd);
                        response = Server.GetClientOutput(ip);
                    }
                }

                if (String.IsNullOrEmpty(response))
                {
                    response = "Internal Error";
                }
            }
            catch (Exception ex)
            {
                Log.Write(ex.ToString());
                response = "Internal Error";
            }
            finally
            {
                context.Response.ContentType = "text/plain";
                context.Response.Write(response);
            }
        }

The Server class calls methods on the PyEngine class to actually execute IronPython method calls:

public static void ProcessInput(string ipAddress, string input)
        {
            PyEngine.CallMethod("game", "ProcessInput", ipAddress, input);
        }

From PyEngine:

public static void CallMethod(string variableName, string methodName, params object[] args)
        {
            try
            {
                ObjectOperations ops = _engine.Operations;
                if (_scope.ContainsVariable(variableName))
                {
                    object instance = _scope.GetVariable(variableName);
                    if (instance != null)
                    {
                        if (ops.ContainsMember(instance, methodName))
                        {
                            object method = ops.GetMember(instance, methodName);
                            ops.Call(method, args);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Log.Write(ex.ToString());
            }
        }

The IronPython Game class ProcessInput method:

def ProcessInput(self, ip, input):
        words = input.split(' ')
        client = self.Clients[ip]
        
        # Update the stored date on the client so it will stay active.
        client.LastUpdateDate = DateTime.Now;
        
        # Get the remainder of the words as a string for chat messages.
        index = 0
        sb = StringBuilder()
        for w in words:
            if index > 0:
                sb.Append(w).Append(" ")
            index += 1
        message = sb.AppendLine().ToString()

        # Setup potential movement realted variables.
        prevRoom = client.player.room
        exit = -1
        direction = ""
        if words[0] == "n":
            exit = 0
            direction = "north"
        elif words[0] == "s":
            exit = 1
            direction = "south"
        elif words[0] == "e":
            exit = 2
            direction = "east"
        elif words[0] == "w":
            exit = 3
            direction = "west"
        elif words[0] == "look":
            client.Write(ROOMS[client.player.room].ToString(client.player))
        elif words[0] == "say":
            # Send the say message to all players in the game.
            for item in self.Clients:
                c = item.Value
                if c.player.name != client.player.name:
                    c.Write(String.Format("{0} says: {1}", client.player.name, message))
                elif c.player.name == client.player.name:
                    client.Write(String.Format("You say: {0}", message))
        elif words[0] == "who":
            # Print a list of players online.
            client.Write("Who's Online:\n")
            for item in self.Clients:
                client.Write("%s\n"%(item.Value.player.name))
        else:
            client.Write("\nInvalid Command\n")
        
        if exit > -1:
            if ROOMS[client.player.room].exits[exit] > -1:
                # Remove the player from the previous room
                if prevRoom > -1:
                    ROOMS[prevRoom].avatars.remove(client.player)
                
                # Add the player to the current room
                client.player.room = ROOMS[client.player.room].exits[exit]
                ROOMS[client.player.room].avatars.append(client.player)
                
                # Output the resuls to the client
                client.Write("You move %s\n"%(direction))
                client.Write(ROOMS[client.player.room].ToString(client.player))
            else:
                client.Write("You can not move in that direction.\n")

The client.Write statements actually pass control back to C# to write the text into a StringBuilder so that all the text can be retrieved at once before sending the response down to the client. On the client side or Silverlight 2 side, input from the user is sent to the server using my HttpHelper class.

private void ExecuteCommand(string cmd)
        {
            HttpHelper helper = new HttpHelper(new Uri((App.Current as App).ServerUrl), "POST",
                new KeyValuePair<string, string>("cmd", cmd));
            helper.ResponseComplete += new HttpResponseCompleteEventHandler(helper_ResponseComplete);
            helper.Execute();
        }

All responses from the server are just output to the chat window:

void helper_ResponseComplete(HttpResponseCompleteEventArgs e)
        {
            switch (_state)
            {
                case GameState.NewConnection:
                    _state = GameState.Name;
                    break;
                case GameState.Name:
                    _state = GameState.Play;
                    break;    
            }

            if (e.Response.Equals("That name has already been used, please choose another."))
            {
                _state = GameState.Name;
            }
            
            this.UpdateConsole(e.Response);
        }

        private delegate void UpdateConsoleDelegate(string text);
        private void UpdateConsole(string text)
        {
            if (this.CheckAccess())
            {
                _content.Append(text);
                txtConsole.Content = _content.ToString() + Environment.NewLine;

                double scrollOffset = txtConsole.VerticalOffset;
                scrollOffset += txtConsole.ScrollableHeight + txtConsole.ViewportHeight;
                txtConsole.ScrollToVerticalOffset(scrollOffset);
            }
            else
            {
                this.Dispatcher.BeginInvoke(new UpdateConsoleDelegate(this.UpdateConsole), text);
            }
        }

The source is not commented very well and lacks consistency because I was trying different things but does lend as a primer and provide something to build from. Of course, this could handle other client interfaces such as AJAX, etc. but I used Silverlight since Perenthia is going to use Silverlight as its UI.

And, because I am good like that, you can download Cameron's Dungeon Source Code (6MB)!

Or just play Cameron's Dungeon!

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

ASP.NET Development | Game Development | General | Perenthia PBBG | Silverlight 2 Development | Silverlight Games

Silverlight 2 and Cross Domain Web Calls

by calbert 3/18/2008 3:49:01 PM

Frank LaVigne has written a nice Silverlight 2 Cross Domain Web Proxy Utility for making any type of web calls from Silverlight 2. This a great utility if you want to serve images or outside content to your Silverlight 2 apps but do not have the ability to setup a domain policy file on the content hosts server.

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

ASP.NET Development | General | Silverlight 2 Development

asp:Silverlight Override

by calbert 3/11/2008 11:01:01 PM

If you are using the <asp:Silverlight/> control to host your Silverlight 2.0 apps from an ASPX page and want to be able to provide a custom splash screen as outlined in Pete Brown's post Xap! App! Pow! Packaging and Application Startup in Silverlight 2 Beta 1 - Part 2 I created the following override that allows you to set the SplashScreenSource and OnSourceDownloadProgressChanged values from within the control itself.

Here is the source for the control:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace Lionsguard.Web.UI.SilverlightControls
{
    [ToolboxData(@"<{0}:Silverlight runat=""server""></{0}:Silverlight>")]
    public class Silverlight : System.Web.UI.SilverlightControls.Silverlight
    {
        protected override IDictionary<string, string> GetSilverlightParameters()
        {
            IDictionary<string, string> dictionary = base.GetSilverlightParameters();
            if (!String.IsNullOrEmpty(this.SplashScreenSource))
            {
                dictionary.Add("SplashScreenSource", this.SplashScreenSource);
            }
            if (!String.IsNullOrEmpty(this.OnSourceDownloadProgressChanged))
            {
                dictionary.Add("OnSourceDownloadProgressChanged", this.OnSourceDownloadProgressChanged);
            }
            return dictionary;
        }

        [Category("Behavior"), Browsable(true), DefaultValue(""), Description("")]
        public virtual string SplashScreenSource
        {
            get
            {
                return (((string)this.ViewState["SplashScreenSource"]) ?? string.Empty);
            }
            set
            {
                this.ViewState["SplashScreenSource"] = value;
            }
        }

        [Category("Behavior"), Browsable(true), DefaultValue(""), Description("")]
        public virtual string OnSourceDownloadProgressChanged
        {
            get
            {
                return (((string)this.ViewState["OnSourceDownloadProgressChanged"]) ?? string.Empty);
            }
            set
            {
                this.ViewState["OnSourceDownloadProgressChanged"] = value;
            }
        }
    }
}

And here is the way that I am using it in my code:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="Perenthia.Web._Default" %>
<%@ Register Assembly="Lionsguard.Web.Silverlight" Namespace="Lionsguard.Web.UI.SilverlightControls" TagPrefix="lg" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Perenthia</title>
    <script language="javascript" type="text/javascript" src="/Common/Scripts/Silverlight.js"></script>
    <script language="javascript" type="text/javascript" src="/Common/Scripts/Splash.js"></script>
</head>
<body>
    <form id="frmMain" runat="server">
        <asp:ScriptManager ID="scriptManager" runat="server"></asp:ScriptManager>
        <div>
            <lg:Silverlight ID="silverlight1" runat="server" Source="~/ClientBin/Perenthia.xap" 
                Version="2.0" Width="800" Height="500" SplashScreenSource="~/Common/Xaml/Splash.xaml" 
                OnSourceDownloadProgressChanged="onSourceDownloadProgressChanged" />
        </div>
    </form>
</body>
</html>

The Splash.js file contains the following JavaScript:

function onSourceDownloadProgressChanged(sender, eventArgs)
{
    sender.findName("uxStatus").Text =  "Loading: " + Math.round((eventArgs.progress * 1000)) / 10 + "%";
    sender.findName("uxProgressBar").ScaleY = eventArgs.progress * 356;
}

Both the Splash.xaml and associated JS are from the Silverlight Beta 1 Quick Start for Displaying a Splash Screen While Loading a Silverlight-Based Application. I will eventually replace the splash with my own Xaml but for now this one works OK for testing. :)

 

**EDIT: The XAP file and the XAML file used to render the splash screen must reside in the same directory, be that the ClientBin or the root.

 ** BETA 2 EDIT: Microsoft has added the SplashScreenSource property and OnPluginSourceDownloadProgressChanged event to their Silverlight control. Kind of makes this one obsolete. I will post more about it soon.

Currently rated 5.0 by 6 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

ASP.NET Development | Game Development | General | Silverlight Games

Silverlight 2.0

by calbert 3/5/2008 2:33:00 PM
Silverlight 2.0 is available for download on silverlight.net!

Currently rated 2.0 by 1 people

  • Currently 2/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

ASP.NET Development | Game Development | General

ToXml Extension Method in .NET 3.5

by calbert 2/27/2008 10:07:00 PM

I built the following extension method in .NET 35. to XML serialize objects; thought it might be useful for someone else. I use to serialize collections to pass XML data to my stored procedures that save player skills and items in my PBBG engine.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
namespace WebTools
{
public static class XmlHelper
    {
public static string ToXml(this object obj)
{
XmlSerializer serializer = new XmlSerializer(obj.GetType());
using (StringWriter sw = new StringWriter())
{
serializer.Serialize(sw, obj);
return sw.ToString();
}
}
}
}

I wrote another extension method specific to retrieving the modified values of a collection. You have to control when the IsDirty flag is set yourself but my properties take care of that.

public interface IModifiable
    {
bool IsDirty { get; }
void MarkClean();
void MarkDirty();
}

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace WebTools
{
public static class ModifierHelpercs
    {
public static List<TSource> Modified<TSource>(this IEnumerable<TSource> source) where TSource : IModifiable
        {
return (from s in source where s.IsDirty select s).ToList();
}
}
}

Currently rated 5.0 by 1 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags: , , , ,

ASP.NET Development | Game Development | General

SQL 2005 XML Data Type, Stored Procedures and Lists

by calbert 11/14/2007 5:41:00 PM

I've seen a lot of stuff out there regarding the SQL 2005 XML data type but most of it is just regurgitates the MSDN documentation. That's fine and all but what about practical uses of it? Well, I have a practical use sample. In building my persistent browser based game Perenthia I have a concept of a Place. A place is a virtual space in which objects are stored. For Perenthia the places represent the various rooms or tiles players move around on. The place or room has exits defined that allow the player to move from one place to the next. The exits are the typical directions; north, south, up, down, etc. In the database I have a Places table and a PlaceExits table. The Places table stores all the information regarding a place and the PlaceExits table stores the placeId along with a directionId and destinationId so I know what exits are available in any room and what rooms they lead to.

The simplified schema for the places would be:

 Places Tables

 In the stored procedure that retrieves the place information I use the following query snippet in the select clause:

    SELECT

        p.*,

    (
            SELECT
                e.DirectionId        AS "@directionId",
                e.DestinationId        AS "@destinationId"
            FROM
                dbo.PlaceExits e
            WHERE
                e.PlaceId = p.PlaceId
            FOR XML PATH('exit'), ROOT('exits')
        ) AS ExitsXml

    FROM dbo.Places p 

 This creates an XML fragment I can then parse in the application to fill a collection of Exits on the Place object.

When saving place information I pass XML generated from the Exits collection in a stored procedure like so:

CREATE PROCEDURE dbo.Places_SavePlace  (@PlaceId int, @ExitsXml xml)

From within the save procedure I perform an update or insert of the place data and then execute the following sql to insert and update the exits for the current place:

    -- Exits

    -- Process the existing exits first

    UPDATE
        dbo.PlaceExits
    SET
        DestinationId    = e.ex.value('(@destinationId)[1]', 'int')
    FROM
        @ExitsXml.nodes('/exits/exit') as e(ex)
    WHERE
        PlaceId = @PlaceId
        AND DirectionId = e.ex.value('(@directionId)[1]', 'tinyint')

    -- Process any new exits

    INSERT INTO dbo.PlaceExits
    (
        ObjectId, DirectionId, DestinationId
    )
    SELECT
        @PlaceId,
        e.ex.value('(@directionId)[1]', 'tinyint'),
        e.ex.value('(@destinationId)[1]', 'int')
    FROM
        @ExitsXml.nodes('/exits/exit') as e(ex)
    WHERE
        e.ex.value('(@directionId)[1]', 'tinyint') NOT IN
        (
            SELECT DirectionId FROM dbo.PlaceExits WHERE PlaceId = @PlaceId
        )

This is working pretty well and keeps me from having to loop through the exits in the application and make multiple database calls. 

PBBG Engine Core Object Structure

by calbert 11/6/2007 1:20:00 PM

The core of my PBBG Engine consists of a Place and an Object. A Place is a spacial definition and can represent a room, world, universe, etc. An Object is anything that can reside within a place, meaning all people, monsters and items are Objects. The Places and Objects are defined using meta data so that properties of an Object are the meta data of the object. This meta data is stored in a seperate table from the object and retrieved as XML when an object is queried. Places work in much the same way with a few pre-defined fields such as the name and x, y, z location of the place. Going this route will allow me to have different objects defined in different games without having null database fields that are not used in all games.

Another example would be a Sword. In Perenthia a Sword dervies from Weapon which dervies from Thing which implements the Object interface required by the PBBG Engine in order to persist the properties of the Sword in the database. I can then create instances of the Sword class when Swords are crafted or purchased. Each level of inheritence can implement properties that will persist for each Sword instance created.

Sword : Weapon : Thing : IObject 

So far it seems to be working pretty well and with the XML features of SQL 2005 the querying of the data is very fast. I am hoping to get Perenthia ported over to this new framework soon as it will provide a more flexible system and allow me to manage game content a little easier. 

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags: , , , ,

ASP.NET Development | Game Development | Perenthia PBBG

Passing Lists to SQL Server 2005 using XML

by calbert 10/31/2007 2:44:00 PM
Jon Galloway wrote up a nice article on passing lists in a stored procedure using XML and SQL 2005's ability to query XML. For anyone who ever wanted to pass an array to a stored procedure this a good read. :)

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags: , , ,

ASP.NET Development | General

Engine Structure

by calbert 10/30/2007 12:05:00 AM

I am structuring my PBBG game engine to be as flexible as possible in order to build various types of games. In order to do that I need to abstract out the components of the engine. Since persistent browser based games are, well, browser based, I decided to follow the normal n-tier model. I am creating a data tier, my actuall database, an application tier which is the engine and will handle client connections, authentication, commands and reading and writing to the database. On top of the application layer will reside the game layer which will be customizable libraries that will use and access the application layer. This follows along the MUD driver and MUD lib pattern where my engine will be the driver which will persist data and handle all communications and my MUD libs or games will be written in an OO fashion to take advantage of the game engine.

The engine is being written in C# and in such a way to take advantage of features of ASP.NET such as HttpModules and HttpHandlers. 

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags: , , , , ,

ASP.NET Development | Game Development

.NET PBBG Engine

by calbert 10/17/2007 11:48:00 PM

In between updating Perenthia and adding new features I have been pulling parts of the code base into a more generic PBBG Engine I am writing in C#. I started working on it when I upgraded the Knights of the Realm game and used parts of it for Perenthia. I am hoping to put Knights of the Realm Beta 2 on the new engine once I get it finished.

I am building the engine as generic as I can but it will incorporate a base rules set and some basic concepts. The base objects will be Avatars, Places and Things. These objects will contain the properties required to function within the rules set and all objects will derive from a base GameObject class that will provide a properties collection for creating custom properties on derived game objects.

The game will be driven by commands sent from the client. Some objects will handle the commands in the engine framework while other commands will cause events to be raised that deriving implementations can handle and provide custom execution or additional execution of the commands.

The egnine will basically be a commands/rules processor that I will hopefully be able to build a variety of games on. I have plans and ideas for several types of games and do not want to continually build the same thing over and over, hence the PBBG engine. 

Powered by BlogEngine.NET 1.3.1.0
Theme by Mads Kristensen

About the author

I am Senior Software Engineer specializing in the Microsoft .NET Framework and PBBG development.

E-mail me Send mail

Calendar

<<  December 2008  >>
MoTuWeThFrSaSu
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar

Recent posts

Recent comments