15 04 2008

My last post was a prelude to this. This weekend I decided to I decided to tackle scripting in a game engine. Specifically being able to talk to an entity and having a flexible system which if necessary could handle Planescape level of jabbering complexity. Originally, I had thought to perhaps use a graph structure in conjuction with some IronPython to do this.

However, I decided why not just have the conversation structure be written in Nemerle as well. Then instead of various scripts and structures to be built at runtime I’d have a compiled dll which could be called into with the added benefit of having also been statically/compile time verified. I also wanted to experiment with how programming in a functional language would aid in game programming. For I have not actually deen any real game related programming that actually used these concepts (really i havent done anything beyond getting irrlicht to load some models in years). As well I wished to try out some of my ideas of Nemerle as a host language for a game. Of course I have no game engine so I decided to do some console based testing.

First I considered the most basic representation of a conversation as both a statement and the set of responses associated with the statement. I also wanted this model to be simple to extend. To start, I decided statements are of type string and responses can be of type () -> string. So each response is a function which when invoked returns the string to be displayed. This of course could not be implemented without entering into circular dependencies so I decided to create a Conversation type/class.

A conversation contains an initial object of type string, (for all c in C, i -> c does exist, sometimes via composition) a terminal object of type () -> string and arrows of type list[string * (Npc -> Conversation)]. I defined it this way in case I ever wanted to create a category theoretic treatment of conversations I had a loose analogy already in place.

So it looks like this:

public class Conversation
        public this()
        public this(s : string)
            initial = s
        public this(s : string, t : response)
            initial = s
            terminal = t
        public mutable initial : string
        public mutable terminal : response
        public mutable arrows : list[string * (Npc -> Conversation)]

Then conversation is always initiated with intial and can be ended at any time with terminal. Initial is then written out with arrows holding the list of responses (a list of functions) that will take one to another conversation and string displaying the current statement. This was then wrapped in a dll. All the conversations would then be held in one or more dlls. As a test I created a module to represent conversations that could be interacted with. The code goes:

namespace Conversations
    public module Guy1
        public Hello(n: Npc) : Conversation     
 	    def k = Conversation()
	    def k.initial = $"Hello $(n.Name), My name is Joe."
            def k.terminal = Bye
            k.arrows = [("What are your plans", Future), ("How is life?",Terrible)]
        Future(n: Npc) : Conversation
            def k = Conversation("The future is a terrible place")
            def response1 = "Why?"
            def response2 = "What is your name?"
            k.terminal = Bye
            k.arrows = [(response1,Terrible), (response2,Hello)]            
        Bye(): string       

Then I could go Guy1.Hello() to initiate conversation and extract the required responses. Next I decided to code a DSL that would make conversations clearer to read. Based on the original code I decided a simple language that mostly served to remove boiler plate code and also make conversations clearer would do.

Nemerle has macros which allow one to do metaprogramming. Not C style but scheme like hygenic macros. The metaprogramming is in two or three parts. The first is of a style that people who have done C++ template metaprogramming would recognize. Namely various compile time execution of certain code that allow various optimizations and new behaviours. The other type is akin to MetaML or Template Haskell and Scheme. Which is the ability to operate on the language’s syntax tree using the language itself and to do various bits and bobs e.g. modify/auto generate code. It also has flexible syntax extension. It is also motivated by MetaML in the idea of having typed code and the notion of execution staging. Being able to operate on, pass around and modify code at compile time is a really nice concept that makes the language flexible. I am experimenting with extending this into runtime – is quite possible.

To continue I arrived at a small set of keywords based on what I had experienced: TlkOf, EndOn, ResponsesOf, Linkers,In, Is, intl, tmnl, put and sc_. sc_ is a short hand macro whose use is sc_[var, string, () ->string]. Which is equivalent to var = Conversation(string); var.terminal = () -> string. Here is a snippet acting as a test:

[Fp] Hello() : ifr 
            TlkOf cnv 
                $"Hello $(n.Name), My name is Joe." 
            EndOn Bye
            ResponsesOf cnv 
                ["What are your plans", "How is life?"]
            Linkers [Future, Terrible]   
[Cp] Terrible() : ifr
            def k = Conversation($"I am dead, $(n.Name)")
            tmnl k Is Bye;
            put [] In k.arrows 

A few things to explain are ifr and the [Cp], [Fp] tags. These names are tentative and are attributes that modify the compile time behaviour of the code. Nemerle does not have top level type inference so having to do all that annotating is annoying, especially as the number of parameters I wish to pass each conversation object increseases. ifr is just a type alias for void and is only there to reduce key strokes…

The Cp means conversaton path, it takes the body of the method, generates a new method with the same body as the old one but also binds whatever parameters to the method I require so I do not have to explicitly declare them in the conversation script – for now just n of type Npc (but will eventually be whatever other ones I decide to add later, this is useful because then scripts automatically benefit and functions wont break when i introduce new parameters – although i might do a few overloads for arrows) and makes the method of type Conversation and makes it public. It then clears the old method. Fp does the same but also initializes cnv (bound to the site) to Conversation() and makes sure the method returns cnv so I dont have to type that. When one decorates their methods with those attributes they must realize also be aware of what variables names have been bound (controlled unhygenics). Ofcourse their use is optional. The dsl thing is abt 50 lines of code. I have the Visual studio integration so I get free syntax highlighting as well.

To actually load the conversation I simply recurse through the paths till I terminate.

Module Program
  Main() : void 
    def p = Npc();
    p.Name  = "Daerax"    
    def say (b, l, i) : list[Npc -> Conversation]
            | s::ss => def (a, d) = s;
                        WriteLine($"( $(i.ToString()) ): $a" )
                        say(ss, d::l, i + 1)
            | [] => l  
        def interact(c)
        def hi = c(p);
        def resps = hi.arrows;
        def l = say(resps, [], 1).Rev();
        WriteLine($"( $( (l.Length+1).ToString()) ) : $(hi.terminal()) \n");    
        def o = int.Parse(ReadLine());
            | [] => ()
            | xs => interact(xs.Nth(o-1))
    def k = 0;               

 public class Npc
        health : int 
        pos : position2D
        public mutable race: string
        [Accessor (flags = WantSetter)] mutable name : string;




Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: