|
Page 1 of 3 Object Oriented Game Programming: Use the Behavior System to share diverse functionality between objects in your game.
Object Oriented Game Programming:
The Behavior SystemThe Behavior System is the single most important idea I've had as a game programmer. They are extremely useful on their own, but they also inspired other powerful systems, most notably Robot Functions and the Scripted Event System. I hope that I can do them justice in this article. What are Behaviors? Behaviors are small objects that add functionality to a parent object. The parent has a reference to the behavior, and the behavior has a reference to the parent, set when the behavior is initialized. The behavior manipulates the data of it's parent object to produce a desired set of functionality. The real strength behind behaviors is that they allow totally different objects to share the same capabilities. For example, the same code (behavior) that slides your menus onto the screen also moves around enemies in the game world. The same behavior that animates your walking player character also animates a glowing effect behind your powerups. The same behavior that makes your widgets clickable can be applied to any rectangular object, and they become clickable too, with just a couple lines of code. If you are familiar with component-based designs, Behaviors could be considered a hybrid between a "pure component" system and a typical class hierarchy. Unlike a pure component system, Behaviors supplement a traditional class hierarchy (rather than replacing it), giving you the full freedom to use "components" AND inheritance to generate objects in your game. Benefits of Behaviors: Behaviors help you transform huge amounts of previously object-specific code into library-code, which can now be shared by many diverse entities in your game, including menus, cinemas, characters, effects, etc. When you add a behavior to an object, you save the time of re-implementing the same functionality again. You avoid bugs and a lot of testing, because you know that the behavior already works elsewhere. If you add more content to the behavior, all of the present and future parents of the behavior get to make use of the new content. Behaviors also keep your parent classes shorter, more organized, and easier to understand by collecting some of the different things that the parent can do and distilling them down into easily managable objects. Origins of the system: The earliest inspiration for behaviors came from an article that I read several years ago about objects in a first-person shooter (I'll try to find the article again so that I can credit the author). I don't remember the specifics, but basically the programmer was describing how objects in the game like the player, pickups, grenades, and enemies don't follow a normal class heirarchy (e.g., they could all extend GameObject and branch as they get more specific), but instead are an aggregate of different chracteristics, like "affected by gravity" and "destructable." This allows objects to only have the attributes that they need. Otherwise, all of the functionality for every object would need to be contained in the top superclass, GameObject, making each individual object much heavier because they have to ignore a bunch of functionality that they aren't actually using. Either that, or you'd have to copy and paste a lot of code to get the objects to behave how you want, which is much worse. This idea seemed really cool, but I couldn't figure out a good way to implement it. The main motivation for behaviors in their current form came near the end of Cash Cow's development when I realized that I had written slight variations on the same code in many different classes. For example, I wrote code to slide the coins to a specific point when they combine, and I wrote similar code to slide the menus in and out. I knew that writing these slight variations on the same code again and again required a whole lot more time, testing, and frustration than if the menus and the coins could somehow share this "sliding" behavior, but how? Thus, the Behavior System was born, as a solution to both of the above situations. Not only do behaviors add attributes to their parents, they can also add more complex capabilities related to game logic, motion, rendering options, and more. It might make more sense with some examples (I'll write all the examples in this article in Java, since I first implemented them in Java). Behavior Examples: One really common behavior is animation. In this case, I mean animating by periodically changing an object's image to the next image in a pre-defined set of frames, like frames of a person walking. Many objects in your game need to do this kind of animation, but they might have nothing else in common. For example, in Primate Panic, the propellers on the plane in the first cinema use an animation behavior to spin, and the chameleons use an animation behavior when the stick out their tongues. The monkeys use one as they climb vines, the humans use one when the throw a monkey, and... you get the idea, they're everywhere. They've saved me a ton of time. While some behaviors are general purpose, like animation, others are more specific and will probably only ever operate on one class. This means that instead of accepting a simple interface as the "parent" object, it would take a reference to a single class, like "Monkey." In these cases, behaviors are still very useful because they allow you to greatly simplify and organize the capabilities of the parent. Here are some behavior examples from Primate Panic (there are dozens more): - A WalkBehavior syncs up the player character's walking frame with the distance in the game world that s/he has actually moved, making it look like the player is really walking on the ground. Monkeys use this too.
- When you click an area on the overworld map, FlipBehaviors "flip" the boxes with all the level icons while changing their image.
- When a monkey pulls on a trap door handle, a TrapDoorBehavior moves the monkey and pull bar down, opens the trap door, and waits a couple of seconds before telling the monkey to drop and the trap door to close.
- A FadeBehavior periodically fades the ghosts in and out, with a little extra code in the Ghost class to activate and deactivate their collision box in the process.
- A ButtonBehavior is responsible for nearly all the logic in the menu buttons. ButtonBehavior detects the mouse's presence, stores button state data, and notifies the parent GUI when it is clicked (see http://rivermanmedia.com/programming/6-programming/5-object-oriented-game-programming-the-gui-stack for more about graphical user interfaces).
Example 2 is a case of a fairly general behavior. Any object that can scale and change images can do the same flipping effect as those icons. This means that if I ever wanted, I could make the monkeys flip like that, the GUI windows, the raft, etc. Same deal with the FadeBehavior for ghosts: any object that can change its alpha value can fade in and out just like the ghosts. All it has to do is implement an interface, CanFade, that contains method stubs for getting and setting its alpha. As for the ButtonBehavior, any object with just a couple important functions, like functions to get its location and dimensions, can become a button just by containing and updating its own ButtonBehavior. Again, this means that, in addition to whatever else they do, spacial objects in any part of your game could also operate as buttons just by implementing a simple interface and updating a ButtonBehavior object. In fact, most objects in your game that store their coordinates and size probably already implement the interface necessary to be a button. What's the point? Without any extra work, a monkey picking fruit could be a button. Folliage could be a button. The shopkeeper could be a button. Darkfellow's grave in the last cinema could be a button. Buttons could be buttons. The list goes on and on. Note: in most of the last examples, you probably wouldn't need to use a ButtonBehavior, but instead a simpler variation that just detects when it's parent is clicked. All the additional state data and graphic changes of actual buttons is more complicated than most clickable things use. That's why I have another behavior, ClickableBehavior, which is a simplification of ButtonBehavior for widgets like the "buttons" on the title screen that don't actually get depressed like normal buttons when clicked. Read on for implementation-->
|
Anyway, love this post, great ideas!
When I think of an Event, I think of any object whose purpose is to represent something that "just happened", and exists to inform an interested listener. An example would be a MouseEvent--when the mouse moves, the system would create a MouseEvent object than describes where the mouse moved to, and may contain other information (such as where it moved from, the current status of the mouse buttons, etc.).
ActionEvents use the same general idea in the context of Behaviors. When the Behavior (which IS an ActionEvent) is finished running, it informs its listener, and passes itself to the listener in the process. Code-wise, the fundamental reason that Behaviors, RobotFunctions, and Effects are all ActionEvents (meaning they implement the ActionEvent interface) is so that ActionLists and ActionQueues can contain ActionEvents as a single type. An ActionList, for example, is really a collection of RobotFunctions, Behaviors, and Effects, but as far as it knows, they are all just ActionEvents. For a full explanation of this ActionEvent idea, check out the Scripted Event System article (http://rivermanmedia.com/programming/28-object-oriented-game-programming-the-scripted-event-system).
I think you are on the right track with the idea to have an AnimationBehavior also be a listener so that it can sync itself up with other aspects of the animation (such as motion). What I might recommend for this case would be to subclass AnimationBevahior and add that idea in (e.g., JumpAnimation extends AnimationBehavior and also implements ActionListener). This way you can use the AnimationBehavior as-is, but also have a special version that's specific to jumping.
Very cool idea!
I've implemented a similar solution in my most recent project, with a few differences. However, what I am now starting to think about is how to make components send events to multiple listeners. For example, say you have a scene the entity is attached to, which gives query methods such as "findEntitiesInRange()" (which could even be a component of the scene). Next, you have a grenade entity with an explode behavior. I'm thinking, you could either query for the entities within range and pass the eventCallback to each of them, or you could pass the explodes behavior (which could have a damage interface) to the scene, and then the scene would query for the entities within range then pass along the event to them.
However, this seems like it could get out of hand quick, doing things this way. I guess you could always type cast the event as a damage type and just use it generically, where as your damage interface would have getRadius, getDamage, etc methods. Am I thinking of this wrong?
Thank you!
The main question is how you want entities in your world communicate with each other. All of your ideas are basically on track with how I would approach it. I deal with this question in basically every game, so here's how I would typically do it working off of your example:
1. The explosion happens. The grenade either knows this itself, or it is told by the ExplosionBehavior, depending on how you want to set it up.
2. The grenade calls Scene.explosion(GameEntity exploder). This tells the "world" (represented by the scene) that there is an explosion happening, and that it was caused by the grenade.
3. The scene now loops through each GameEntity, calling GameEntity.respondToExplosion(GameEntity exploder), where exploder is the grenade. The GameEntity superclass contains the respondToExplosion() method, so all individual game entities are guaranteed to respond in some way.
4. Each GameEntity now responds to the explosion in its individual respondToExplosion() method, which it overrides from the parent GameEntity class. GameEntity has a default implementation of this method, so only the objects that want to do something different from the default need to override it. For example, characters and enemies override the respondToExplosion() method by taking damage and blinking, non-destructible objects are pushed back but not damaged, and nearby terrain might have a smoking effect but are otherwise unchanged.
But suppose your entities need to do something different depending on what object caused the explosion. Because you are passing the grenade object through as a parameter (the exploder), the entities can inspect the grenade and determine how to respond appropriately.
There are also ways you can vary this basic idea to suit your needs better. Maybe you don't want each individual object to have to check if the grenade was close enough to actually cause damage, so you can have the explosion pass in a radius to the scene, Scene.explosion(GameEntity exploder, double radius) and the scene would only call GameEntity.respondToNearbyExplosion(exploder) on objects that were close enough.
If you needed the GameEntities to know even more about the explosion, you could create a small ExplosionParameters class like what you talked about, with getRadius(), getDamage(), getType() methods etc. and pass this parameter along to the scene and GameEntities, which they can inspect and decide how to respond.
Does that help at all?
I then created a "BasicSceneManager" class which extends Entity (which entity implements eventlistener). The scene manager has a list of scenes and method called "notifyScenes" which is called in the scene managers eventcallback method and is triggered via a "WatchEventBoardBehavior" though I don't think that qualifies as a behavior...
The scene class "BasicScene" then does the same basic thing but with entities. Entities then in turn, of course have their event callback method. Now, this is where I am currently at and ties in with what you were speaking about as far as object/object interaction.
What I am thinking is having a "ReactionEvent" or similar. Mixing this with the explosion example, the explode behavior would notify the event board that an explosion has occurred, this event will be passed down through the scene manager, then to the scenes, and then to the entities. Now, somewhere in there, you could also account for spatial partitions as well..but for now, all entities will receive the event via their eventcallback method.
This is where ReactionEvent comes into play. Reaction events can be added to a list and then looped in theory. Their constructor would be the actionlistener (the entity) and then an event callback method as well.. Now inside of these reaction classes, and please correct me if this is bad design, they could check the type of event/listener via "instanceof" and then call whatever methods they need... so for example:
damage = ((ExplodeBehavior) event).getDamage();
entity = ((SomeEntityType) listener).applyDamage(damage);
Where applyDamage is an interface method. All of this would be checked with multiple instanceof checks. Now, stuff such as collision detection / response is coming to mind now, but really there isn't too much difference between the explode behavior and say a CollisionBehavior.
What do you think?
I'm not really sure why, but I've always had a strong aversion to casting objects. I was using casts in two places in my library code before (I think I had examples in the GUI Stack article and Scripted Event System article) but I've since found ways to remove the casts. I've also ran into cases that instanceof did not return the value I was expecting in testing. Many programmers dislike casting and instanceof because they tend to be very slow operations (even in other programming languages), but for me it's more of a design concern. Strongly typed languages like Java are designed so that you should rarely if ever need to circumvent the typing system and do a cast (especially languages with templates or generics, which Java does have). Working with the typing restrictions rather than against them allows you to fully leverage the compiler's ability to ensure that your design is sound.
That said, there can be times where it is difficult to find a more elegant solution than a cast, and sometimes the most elegant alternative does come with drawbacks. If you do feel like casting is the best option, you can still avoid using instanceof if you want by implementing your own simple typing system. Each class that you might have to cast would extend a superclass (or interface) with a method called getType() that returns an int, and you can check the type based on this int, which will be faster than instanceof and easier to debug.
Either way though, good luck with that system! Let me know how it goes.
Specifically look at:
-----------------------------
public void draw(List shape) {
// rest of the code is the same
}
Here is another example of a generics method that uses wildcards to sort a list into ascending order. Basically, all elements in the list must implement the Comparable interface.
-----------------------------
We're discussing this a lot here, if you'd like..email me at jasonw dot developer at gmail dot com. (sorry, don't wanna get spammed) lol.