Point-and-click architecture


(Jacob Albano) #1

Continuing the discussion from Humphrey’s Tiny Adventure:

Figured I’d make this a new thread for this, since it’s likely to get long.

So the main thing that’s important to understand about a game like you’re looking to make is that you’re going to have a lot of objects that are functionally identical. There’s no effective difference between the keyring you find in one zone and the keycard you find in another - they both fulfill the same purpose: when object A interacts with object B, do action C. With this in mind, you don’t need to have a separate class for every item – all you need is for every item to have a unique string identifier,

For both Humphrey’s Tiny Adventure and Hypothermia I used a fully data-driven approach where I only ever had one world class (in Humphrey’s case, this applies to the normal view). All my objects were placed with Ogmo and I used Slang for scripting, though it’s entirely possible to skip on proper scripting if your use cases are simple enough.

For interactions (like Key + Door = Door unlocked) I placed an entity called WorldReaction which has two properties: the identifier of the object that it responds to (the Key) and the filename of the script file that should run. In Slang, such a script would look something like this:

removeInvItem "doorkey"
remember doorOpen

Basically, all I’m doing here is removing the inventory item that was used (the “doorkey” item) and then storing a value in the save file that says the door is open. Next time the scene is loaded, the door will open without the key. Again, it’s possible to do this without a scripting solution, but I found it to be very helpful.

Placing objects is a little abstract. Since you don’t want to have to create a new Ogmo entity type for every item you’ll have in your scene, the best way in my experience is to simply have a WorldItem type with a property for the definition file that the item will use. Something like this:

<item>
    <id>front door key</id>
    <image>rusted_key.png</image>
    <isInventory>true</isInventory>
<item>

The upside to this approach is that it’s much less work to create new entity types for objects you’ll only ever place once. The downside is that your editor ends up looking like this:

Well worth the tradeoff, in my opinion.

Since this is a very dynamic approach, the ability to resolve assets by name is indispensable. I always use my custom library loader FLAKit when developing in Flash, so this is baseline for me. You can implement this however you like, but the bottom line is that you need to be able to do this:

var nextLevel:XML = loadXML(hotspot.nextLevel);
var itemImage:BitmapData = loadBitmapData(item.imageName);

As a bonus, using FLAKit means that at any time you can press a button and reload all your assets without closing the game. It’s really handy for tweaking item positions or seeing how an image looks in-game while you’re working on it.

Finally, a data-driven approach favors autonomy. You won’t be able to have much logic in your top-level World class, if any at all. Design your entities to handle their own collision, mouse logic, and so on. Hopefully you aren’t given to relying on static variables, because they won’t be an option any more. When you need to communicate between entities, use message passing (I can go into detail on that if need be).

Hopefully that’s enough to get you started! Let me know if you have any other generic questions.


Camera point &click game [continued in point-and-click architecture thread]
(s corneil) #2

Thanks for the long post ! Lots of new information in there for me …

I’m a bit confused about this part : … WorldItem : <item> ... </item>
Does that mean you do not add the items the player can interact with in the Ogmo editor ? Or, if you do add them (but not as entities), then … as what type ?
Am I correct if I assume this code is part of the XML that Ogmo would normally generate for us? Except that you have added this info manually ?

Still need to look into FLAKit … so I can’t ask you questions about that yet . :smile:

But : thanks a lot !


(Nicole Brauer) #3

Really interesting post! Thank you for taking the time to explain it all. Since I’m currently working on a p&c game as well this has been a valuable insight. :smile:

I would like to hear a bit more about how you handle “message passing”.

(Also since I just saw that Slang 3.0 is writting in Haxe: Does your FLAkit also work with Haxe?)


(Jacob Albano) #4

The bit with the manual XML markup deserves a little more explanation, I apologize. The idea is that you have a basic WorldItem entity in your Ogmo project, and it has a string property on called “typeName”. When you place the entity in the editor, you fill out that field:

So then when you load your Ogmo level, you load the corresponding item file, which might look like what I showed above:

<!-- front_door_key.xml -->
<item>
    <image>rusted_key.png</image>
    <isInventory>true</isInventory>
<item>
var item:XML = library.getXML("items/" + String(entity.@typeName) + ".xml");
var image:Bitmap = library.getImage("art/items/" + String(item.@image));
var isInventoryItem:Boolean = item.@isInventory == "true";

add(new WorldObject(image, isInventoryItem))

Of course, it’s possible to have all these properties on your Ogmo entity itself, instead of in separate files. I’ve done it both ways and this is my preference. It’s especially helpful if you plan on having multiple instances of the same type, like coins to collect, since you don’t have to worry about making sure each one is the same every time.


(Jacob Albano) #5

FLAKit is Flash-only at the moment. If you’re interested in using it with Haxe I’m sure it won’t be that hard to port; at the moment it’s been reduced to one file. Otherwise, the OpenFL Assets system is similar (though without live asset reloading, alas).

Aside, I may end up rewriting Slang in Flash again. I wrote it in Haxe because I thought it would make it simple to use on a variety of platforms, but the Flash support isn’t great – property accessors are visible, and the documentation comments don’t show through at all. Even I was having trouble using it when I tried recently, so I’ll bet it would be close to impossible for anyone who didn’t have intimate knowledge of how it works. For my Haxe projects going forward I’d probably use Hscript instead, since the syntax is nicer and has support for many more features.

So, message passing. It’s a very generic term that can mean a lot of things depending on who you ask – publisher/subscriber model, direct dispatch, observer pattern, etc etc. When I talk about it I’m referring to wide-broadcasts with optional direct-dispatch (the terminology! it’s too much).

At a very basic level, message passing is a very dynamic way of calling functions. Here’s an example of what it looks like:

// in your guard entity
addResponse("world sound", onHearSound);

private function onHearSound(soundX:Number, soundY:Number):void
{
    if (FP.distance(x, y, soundX, soundY) < 200)
        trace("heard a sound!");
}

// in your sneaky burglar entity
// whenever you take a step
broadcastMessage("world sound", x, y);

Effectively, all that’s happening is that every entity in the world is sent the message type (“world sound”) and the payload ([x, y]), and if it has a response to that message, it calls the appropriate function or functions. When I work with Flashpunk, I use a few custom classes that have this all built in.

The most interesting aspect of it in my opinion is that you it facilitates emergent behavior – or at least cuts down on the work you have to do. Take the example above: with a normal approach, you might have to run a loop through all the nearby guards when you take a step, to see if they heard it. That’s all well and good, but now you want to have them also react to gunshots…and glass breaking…every time you add a new system you have to go back and hook everything up to it. With message broadcasting, all you have to do is make your noisy objects broadcast the “world sound” message, and everything else just magically works.


(Mike Evmm) #6

I like that architecture a lot, though it reminds me of flash’s ‘listeners’, so how do you go about implementing it? Also, how would you recommend loading assets dynamically w/o FLAKit (so as for a release version)?


(Jacob Albano) #7

This should have been a response to @miguelmurca.

Event listeners are very similar to this system; they would fall into the “publisher/subscriber” model of message passing. The main difference here is that it’s much easier to set up – there’s no need to remove event listeners because a message response will only be executed if the entity it’s on is in the world. I’ll put together a small implementation to show you how it works.

For release mode, FLAKit creates an as3 class with all your files embedded. You use the same interface to retrieve assets no matter whether you’re using embedded mode or dynamic mode. Here’s the one it made for Humphrey’s Tiny Adventure, though the approach has changed a bit since then. To load embedded assets now, you just do this:

library = new Library("../lib", Library.EmbedMode);
library.loadEmbedded(new EmbeddedAssets());

(Jacob Albano) #8

Here’s my simple (hah) implementation of a message passing system. All you do is use these classes wherever you would have extended an Entity or a World. I’ve tested all this, but not exhaustively, so there may be some issues I didn’t account for.

(attn. @VoEC as well)

World wrapper, MessageWorld.as

package 
{
    import net.flashpunk.World;
    import net.flashpunk.Entity;
    
    public class MessageWorld extends World
    {
        private var allEntities:Vector.<MessageEntity>;
        /**
         * Constructor.
         */
        public function MessageWorld() 
        {
            allEntities = new Vector.<MessageEntity>();
        }
        
        /**
         * Broadcast a message to all MessageEntity instances in the world.
         * @param    messageName The name of the message to broadcast.
         * @param    ...args The parameters to pass to the message responses.
         */
        public function broadcastMessage(messageName:String, ...args):void
        {
            for each (var e:MessageEntity in allEntities) 
            {
                e.onMessage.apply(null, [messageName].concat(args));
            }
        }
        
        public override function add(e:Entity):Entity
        {
            if (e is MessageEntity)
                allEntities.push(e as MessageEntity);
            return super.add(e);
        }
        
        public override function remove(e:Entity):Entity
        {
            if (e is MessageEntity)
            {
                var index:int = allEntities.indexOf(e as MessageEntity);
                if (index >= 0)
                    allEntities.splice(index, 1);
            }
            
            return super.remove(e);
        }
    }
}

Entity wrapper, MessageEntity.as

package  
{
    import flash.utils.Dictionary;
    import net.flashpunk.Entity;
    import net.flashpunk.Graphic;
    import net.flashpunk.Mask;
    
    public class MessageEntity extends Entity
    {
        private var responses:Dictionary;
    
        public function MessageEntity(x:Number = 0, y:Number = 0, graphic:Graphic = null, mask:Mask = null) 
        {
            super(x, y, graphic, mask);
            responses = new Dictionary();
        }
        
        /**
         * Broadcast a message to the world this entity is in.
         * @param    messageName The name of the message to broadcast.
         * @param    ...args The parameters that make up the payload of the broadcast.
         */
        public function broadcastMessage(messageName:String, ...args):void
        {
            var broadcast:MessageWorld = world as MessageWorld;
            if (broadcast != null)
            {
                broadcast.broadcastMessage.apply([messageName].concat(args));
            }
        }
        
        /**
         * Called by MessageWorld when this entity is to be sent a message. You can also call it yourself to manually invoke its behavior.
         * @param    messageName The name of the message to send.
         * @param    ...args The parameters to pass the message response.
         */
        public function onMessage(messageName:String, ...args):void
        {
            var response:MulticastDelegate = responses[messageName];
            if (response != null)
                response.apply(args);
        }
        
        /**
         * Add a message response.
         * @param    messageName The name of the message to respond to.
         * @param    func The function to call when the message is recieved.
         */
        public function addResponse(messageName:String, func:Function):void
        {
            var delegate:MulticastDelegate = responses[messageName];
            if (delegate == null)
            {
                delegate = new MulticastDelegate();
                responses[messageName] = delegate;
            }
                
            delegate.add(func);
        }
        
        /**
         * Remove a message response.
         * @param    messageName The name of the message to respond to.
         * @param    func The function to be removed.
         */
        public function removeResponse(messageName:String, func:Function):void
        {
            var delegate:MulticastDelegate = responses[messageName];
            if (delegate != null)
            {
                delegate.remove(func);
            }
        }
        
    }

}

Multicast delegate class, MulticastDelegate.as

This is a bit of a bonus, since it’s completely standalone and could be used anywhere. Basically allows you to group together multiple similar functions and call them at the same time.

package  
{
    public class MulticastDelegate 
    {
        private var functions:Vector.<Function>;
        
        /**
         * Constructor
         * @param    ...funcs A series of functions, or an Array or Vector.<Function>
         */
        public function MulticastDelegate(...funcs) 
        {
            functions = new Vector.<Function>();
            
            if (funcs.length > 0)
            {
                if (funcs[0] is Array)
                {
                    functions.push.apply(functions, funcs[0] as Array);
                }
                else if (funcs[0] is Vector.<*>)
                {
                    for each (var func:Function in funcs[0] as Vector.<*>)
                    {
                        functions.push(func);
                    }
                }
                else
                {
                    functions.push.apply(functions, funcs);
                }
            }
            
        }
        
        /**
         * Add a function to the delegate.
         * @param    func The function to add.
         */
        public function add(func:Function):void
        {
            if (functions.indexOf(func) < 0)
                functions.push(func);
        }
        
        /**
         * Remove a function from the delegate.
         * @param    func The function to remove.
         */
        public function remove(func:Function):void
        {
            var index:int = functions.indexOf(func);
            if (index >= 0)
                functions.splice(index, 1);
        }
        
        /**
         * Call all functions in the delegate with a series of parameters.
         * @param    ...args Parameter list.
         * @return An array containing each function's return values. If a function returns void, no value is stored.
         */
        public function call(...args):Array
        {
            return apply(args);
        }
        
        /**
         * Call all functions in the delegate with an array of parameters
         * @param    args Parameter array.
         * @return An array containing each function's return values. If a function returns void, no value is stored.
         */
        public function apply(args:Array):Array
        {
            var result:Array = [];
            for (var i:int = 0; i < functions.length; i++) 
            {
                var retval:* = functions[i].apply(null, args);
                if (retval != undefined)
                    result.push(retval);
            }
            
            return result;
        }
    }
}