Author Topic: clean code  (Read 28993 times)

Leaf

  • Rogueliker
  • ***
  • Posts: 64
  • Karma: +0/-0
    • View Profile
    • Email
Re: clean code
« Reply #15 on: August 15, 2012, 04:08:18 PM »
I think the cleanest way to implement a game like this would be to use an OO language to build an engine with an event-driven paradigm around various interfaces.  Perhaps with some sort of compositional object model to get rid of the boilerplating problem.
That is quite high level, could you elaborate it a bit?

Define interfaces to "listen" for particular events and "extract" particular data.  Write your objects that inhabit the game world so that they implement those interfaces.  For example, "public class Torch implements Item, Light, HoldableOneHanded".

The Light interface might define method prototypes for getting brightness, radius, etc.

The HoldableOneHanded interface might define no method prototypes, but the game code could check what interfaces an object implements to see where it can be worn/wielded.

The Item interface might just extend a set of other interfaces, such as Gettable, Droppable, Description, Name, Weight, etc.  All of those interfaces would define appropriate method prototypes.

All those would be interfaces for extracting data.  When a player moves, all of the objects that implement Light within the scope of the location that the player has moved to could be queried to calculate what the player can see.  When the player tries to pick up an item, the game engine could look and see if the object implements the Gettable interface before letting the player pick it up.

A monster might implement an event handler interface, such as "OnObjectMoved".  That OnObjectMoved interface might define an "onObjectMoved( Object movingObject )" method prototype, to be called whenever something moves around in the dungeon, be that a player or whatnot.

Then your game engine has an event mechanism, where when, say, an object moves, the game engine collects a set of objects within the scope of the location that the object is moving to.  It loops through them and finds the ones that implement the OnObjectMoved interface and calls the event handler method defined by that interface.  Within each object's event handler method body would be the code to respond to the event.  A monster could "see" the player and move towards him, or attack if close enough.

You could define many different sorts of higher-level event interfaces, like for when an attack happens, damage is dealt, the player gets hungry, etc, etc, etc.  Objects in the game world can define handlers for these events, so that they can respond to them appropriately.

The event-driven paradigm is very powerful, allowing you to cleanly define object behaviors without junking up the core of the game engine with a bunch of stuff that you have to sort through when modifying your code.  It makes maintenance much easier and allows for a great deal of extendability without having to touch the core game engine code.

This all seems fine and good, but about the time you write the same boilerplate code for the 50th monster or the 20th weapon, you're going to be gnashing your teeth!  That's where the compositional object model comes in......

Instead of directly defining a class for every sort of object in the game, we define a "GameObject" that is nothing more than a set of "Properties" (a slightly inaccurate term, given its use in newer languages like C#, but I borrow it from CoffeeMUD).  Then you write a bunch of generic properties that implement the various interfaces and take various initialization values.  "public class StdLight extends Property implements Light".  The constructor for StdLight could take brightness, radius, and duration values, or something.  There could also be various different properties that implement various event handlers to define monster behaviors.  A goblin archer could have standard properties like "RangedCombatant, Cowardly" etc to define their behavior, while a goblin axeman could have properties like "MeleeCombatant, Aggressive" etc.

Then your actual items, monsters, etc in the game become a set of XML files that list a bunch of different properties and their initialization values.  You can make new objects quickly without a bunch of boilerplate code, maybe even in-game with a simple built-in object editor, by just assigning a bunch of properties with initialization values to an empty GameObject.

And since all of these properties are backed by data extraction and event handler interfaces, you can still define special properties to provide special behaviors to very unusual objects, and they just magically work with the game engine.

Since the properties are so reusable and can be extended to create those special behavior properties for special items and monsters, they can also /define their own behaviors/ internally, instead of junking up innards of the game engine with movement code, combat code, etc.

Thus the whole clean abstraction thing that is sometimes so very difficult to preserve in game programming is preserved, and you have a very modular game engine that can be easily customized just by modifying some of the low-level properties that everything else is built on.

The compositional object model is great for generating random monsters and magical items, too.  Your generator just picks some properties off a table and assigns them.  Done.  No weird stuff to mess with deep in the game innards.

I think it may be overkill for a "small" game, but for a "large" game, I think such a design can really help keep the implementation from collapsing under it's own weight, especially if you are doing it all for fun and just designing as you go instead of making up a big ol' design document and sticking to it.
« Last Edit: August 15, 2012, 04:13:53 PM by Leaf »

tuturto

  • Rogueliker
  • ***
  • Posts: 259
  • Karma: +0/-0
    • View Profile
    • pyherc
Re: clean code
« Reply #16 on: August 16, 2012, 05:30:22 PM »
That sounds pretty good and flexible. Different kinds of game objects would be easy to construct and they would follow same rules. Definitely a good approach I would think.

That's half of the solution, you still need a game engine that is clean and easy to maintain.
Everyone you will ever meet knows something you don't.
 - Bill Nye

Leaf

  • Rogueliker
  • ***
  • Posts: 64
  • Karma: +0/-0
    • View Profile
    • Email
Re: clean code
« Reply #17 on: August 16, 2012, 10:22:10 PM »
The way I did the event-driven thing for my stuff, the core of the game engine just became a relatively small multithreaded (it was multiplayer, with one "engine" running for each area, with some RPC-type operations to move objects between different areas) timer/turn event dispatching loop and object graph management thing.  Things like the combat system and magic and stuff got implemented in various object properties.  I don't know if one could call it perfectly "clean" or not, but it was all fairly modular and not all tangled together.

The player wasn't any different than a monster, except that the monster AI property was replaced with a property that handled translating player input packets into events generated by the player object, while listening for events and sending screen update packets to the player's client based on the events it heard.  Actions/Skills/Powers available to the players/monsters were implemented as separate properties, either properties on the "creature" itself or on objects in its inventory (ie, if you're wielding a sword, the sword provides the melee attack action), so that the AI could be kind of generic and pick actions/powers out of broad categories (melee attack, ranged attack, AOE, escape, etc) instead of being hardwired with a bunch of different specific actions/powers.

Unfortunately I've never completed the game, because I keep thinking that there is a better way to implement everything and starting over from scratch, after taking breaks in-between.  I started the first one 8 or 10 years ago.  I think the last time was the 12th partial-rewrite from scratch. >_>

So I guess what I am getting at with all that is, maybe there is a trade-off to be made between making it "perfect" and "clean" and actually completing something. <_<

lithander

  • Newcomer
  • Posts: 25
  • Karma: +0/-0
    • View Profile
    • pixelpracht.net
    • Email
Re: clean code
« Reply #18 on: August 17, 2012, 12:08:43 AM »
Nice posts, Leaf. But a word of warning: Designing a good game (from a game designers perspective) is hard! Good software design is hard too. Doing both at once? Madness! :)

I don't disagree with what you said. Component based design is great if you want to implement hundreds of different items, monsters and other game objects. In that case (in most cases actually) composition beats inheritance. And interfaces and events are great ways to decouple those components so they remain flexible. All that stuff is true. And it's good to know!

But when your goal is to make a game, any solution that solves the problem and performs good enough actually is good enough. You might have heard the quote "Premature optimization is the root of all evil" and while it originally was made in the context of performance optimizations I think it holds some truth in regards to software design. It's hard to come up with the perfect engine that will solve all your potential needs and problems before you even know what they are. So my suggestion would be to just start coding. Don't aim to write final quality code in your prototypes (and DO prototypes first!!). Instead prepare to reimplement or refactor critical parts when you know more specifically what you need and where the bottle necks are. Refactoring is so easy in modern IDE's that it won't take too much time. Of course, with experience you'll be able to take shortcuts, design your software in a way that will avoid problems you experienced with earlier projects, that will allow your project to remain flexible and maintainable. But it's hard to get that knowledge from reading tutorials. I'd say that when you want to learn game programming spend 90% coding and 10% reading.

Leaf

  • Rogueliker
  • ***
  • Posts: 64
  • Karma: +0/-0
    • View Profile
    • Email
Re: clean code
« Reply #19 on: August 17, 2012, 01:42:41 AM »
But but but but....

Designing and writing overly-complicated game engines is so fun!  ;D

When it comes to making actual playable games with the engine, I start getting bored and chase down some other shiny thing, lol.....

Alex E

  • Rogueliker
  • ***
  • Posts: 118
  • Karma: +0/-0
    • View Profile
    • Email
Re: clean code
« Reply #20 on: August 17, 2012, 01:48:15 AM »
Unfortunately I've never completed the game, because I keep thinking that there is a better way to implement everything and starting over from scratch, after taking breaks in-between.  I started the first one 8 or 10 years ago.  I think the last time was the 12th partial-rewrite from scratch. >_>

That's one of the problems you may face when creating a game. You start to lose motivation after a while. Some people don't, but I usually do after a few months.

tuturto

  • Rogueliker
  • ***
  • Posts: 259
  • Karma: +0/-0
    • View Profile
    • pyherc
Re: clean code
« Reply #21 on: August 17, 2012, 04:57:16 AM »
Good stuff indeed. Treating player character in a same way as all the other character (be they monsters or other players in multiplayer), also makes things much simpler and cleaner.

I'm a fan of iterative development. Usually I aim to write best what I can at any given time, but don't worry about future too much. Like I haven't really given any thought on ranged combat yet, because there's so much other things to work with first. But when I get there, I try to make that as good and clean as possible. This of course means that I have to change existing code sometimes to make the new design as good as possible at that given time.

Keeping steps small seems to help with motivation for me. I get something new relatively fast and if feature I chose turns out to be bad idea, I can just discard it without losing too much time and effort.

Do you happen to have any source code available Leaf that I could have a look at?
Everyone you will ever meet knows something you don't.
 - Bill Nye

Leaf

  • Rogueliker
  • ***
  • Posts: 64
  • Karma: +0/-0
    • View Profile
    • Email
Re: clean code
« Reply #22 on: August 17, 2012, 05:44:42 PM »
I have it all in a subversion repository going back a few years.  The oldest stuff is on tapes somewhere and I no longer have a tape drive that will read them, but that stuff was all in C/Unix and fugly anyway. :P

I've never publicly released any of it though.  Let me think about it.  What parts in particular are you interested in? :P

Something that I kept going round and round and back and forth about is how to "stack" properties on the object, how/what order to route events to them (first property of type?  All properties of type?), and how to let the higher level game developer (let's call him the "world scripter") easily call a method defined on some property on an object without having to do a bunch of instanceof stuff or check for null values (making the assumption that the "world scripter" may be at a neophyte level of programming experience and needs something that can still be reasonably robust even when they write bad code).  Seems to be a toss-up between doing it "right" and making it "easy" there...

tuturto

  • Rogueliker
  • ***
  • Posts: 259
  • Karma: +0/-0
    • View Profile
    • pyherc
Re: clean code
« Reply #23 on: August 18, 2012, 07:10:50 AM »
I'm interested on the object model (characters, items, rooms and such) and how you get them working together (characters walking around and fighting with each other).

Thinking from point of view of other person using your engine is good. They probably don't have the same knowledge as the person who wrote it after all. That's an interesting question actually, how much of bad code should the engine tolerate? With languages that are compiled to create an executable, you can at least lean on the compiler to tell you if you're doing something really stupid. With interpreted languages first sign of error is often runtime crash.

There's this short poem that gets shipped with standard python implementation.  Currently I like especially the two lines about explaining the implementation.

The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
Everyone you will ever meet knows something you don't.
 - Bill Nye

Leaf

  • Rogueliker
  • ***
  • Posts: 64
  • Karma: +0/-0
    • View Profile
    • Email
Re: clean code
« Reply #24 on: August 18, 2012, 03:26:31 PM »
At the risk of being a Java fanboy, I think java can give you the benefits of both.  (Though honestly I prefer C# as a language, but it doesn't run on near as many platforms.)

Mine was designed to be a multiplayer engine with online content creation.

- You can force untrusted Java classes to run in a sandbox, great for user-authored code running server-side.
- You can load those classes from anywhere you want (ie database or "user home directories" in a virtual filesystem, or even over the network) by writing custom classloaders.
- There's a compiler built into the runtime libraries that is easy to use.  No shelling out to the OS and doing other sorts of nonportable stuff to compile properties written through an OLC.  My plan was to have a dedicated compiler thread that all compilation jobs got serialized through, do avoid bogging down the game if everyone was compiling at the same time.  But I didn't get that far.
- Since the OLC stuff is compiled and runs through the hotspot JIT, it's as fast as native code (except for a bit of securitymanager overhead if you are running them in a sandbox).
- Since it is compiled, you get the syntax errors out of the way ahead of time.


I'll see if I can dig up some of my old documentation on the object model.  I am fairly sure it's in svn or backups somewhere. :P

In the meantime, if you're familiar with Inform 6, I kind of stole part of the object model from the Infocom Z-Machine, with some special behavior at the head of the tree to arrange the world into a grid instead of an arbitrary graph of spaces.  So I guess it's more like a 3d grid of trees.

I am feeling incredibly lazy today, so I probably won't be able to find the gumption to dig for docs until I have more coffee in me....  Maybe tonight.

Leaf

  • Rogueliker
  • ***
  • Posts: 64
  • Karma: +0/-0
    • View Profile
    • Email
Re: clean code
« Reply #25 on: August 18, 2012, 04:38:28 PM »
Alrighty....  I violated the implementation lines of your poem, but (weakly) in my defense, most of the fun was trying to make the game engine highly concurrent and distributed, so that always adds some complexity and indirection, to ensure that threads don't mess with each other's data directly and cause nasty race conditions and concurrent modification exceptions. :P

At the top of the object graph was something I called the "Realm".  The realm contained a bunch of areas, or "Dungeons".  The realm provided methods to look up dungeons by name, returning a reference to a dungeon.  It also provided methods to start, stop, create, and delete dungeons.  When the realm booted, it read a directory hierarchy off disk that contained the game state and used that data to initialize the dungeons back into whatever state they were left in when the realm was last shut down.

Each Dungeon ran in its own thread, and Dungeons could not access each other's object graphs.  The only globally accessable methods provided by the dungeons were methods that they could use to pass Tasks (glorified Runnables) to each other through concurrent queues.  The idea here was to eventually be able to replace all of the shared memory operations above this level with stuff being serialized over TCP sockets, so that a realm could run on a cluster/cloud, or you and your buddies could link your realms together over the internet into one big game world.

Internally, the dungeons maintained a 3d grid of GameObjects in a threadlocal hashmap keyed with 3d integer coordinates.  These toplevel GameObjects represented "cells" on the map.  The dungeon also internally maintained a table of GameObjectData (private), keyed by UUIDs, and all internal GameObject operations looked up data on this table by that key, instead of holding direct references to other GameObjects.  This bit of indirection was to enforce that dungeons could /only/ access game objects contained within themselves.  This made higher level property programming simpler, because the world builder could write their properties without having to worry about narsty concurrent programming issues.

GameObjects provided methods to look up their Parent (the GameObject that contained them) and their Children (the GameObjects that they contained).  They also provided methods to move themselves to a new parent.  A GameObject's parent and children were not directly modifiable; you could only move them to a new parent.  This was to ensure that the object graph never became circular (example: a chest containing a bag, while the bag also contained the chest, which would have caused code that walked up and down the object tree to get stuck in an endless loop).

GameObjects also provided methods to add and remove Properties from them.  So if I wanted to make a wall at coordinate (0,0,0), I'd create a new GameObject, add an Obstacle property, a Graphic property, and maybe a Name("Wall") property to it, and then move the GameObject to (0,0,0).

Early implementations used a global Heartbeat event, but I later removed that as it proved to be too CPU intensive with large worlds.  So Properties could register a Timer event, either repeating or one-shot, and an OnTimer event would be raised to that property at the given interval.  This is how monsters wandered aimlessly around the dungeon waiting for players to run into them.

Some event handlers could return a boolean value that could signal whether the event chain was to be interrupted or not.

For example, when an object was about to move, it would raise an OnMoving event to the set of all objects near it and all objects near its destination.  Any of these OnMoving event handlers could return true, in which case the event chain was canceled and the object did not move.

This was handy for this sort of use-case:  A player is standing next to a wall.  The player pushed a direction key, trying to move into the space occupied by the wall.  The wall gets the player's OnMoving event.  Seeing that the player is trying to occupy the space that the wall is in, the wall prints a message to the area saying, "So-and-so runs into the wall and falls down!".  Then the wall returns true, canceling the event chain and preventing the player from moving.

(Edit: I think the key thing here regarding cleanliness, that event driven programming is really good at is....  Notice that the wall's behavior (blocking movement) is implemented /by the wall/, not by the movement code on the player.  If I go to change how walls work, it's all there in one place.  There is no spaghetti of stuff between the wall and the player movement code and your toaster and the neighbor's dog.)

Once an object had successfully moved, another event, OnMoved was raised to the area.  Monsters used this to notice when other things wandered around the dungeon, so that they could see players coming and decide to either flee or become aggressive.  Players also used this to send screen update packets to their clients.

There were other low-level events that were raised by the engine, things like properties being added or removed and such.  I never used them for much, but the idea was that critters would be able to notice when spells wore off and re-cast them and stuff like that.

Higher level events (such as combat and damage events, speech, etc) were raised by other object properties rather than the low-level engine.

Since dungeons were only allowed to know about objects that they contained, the issue of moving objects between dungeons was handled by the dungeon Tasks mentioned above.  An object (and its children) to be moved to another dungeon was converted into an "object blueprint" and removed from the dungeon's object table.  The blueprint was then packaged up into a Task, which was enqueued to the other dungeon.  The new dungeon would dequeue the Task in its own thread and run it.  The run() method of the task would then rebuild the object (and its children) in the new dungeon from the blueprint.  Since the actual "GameObject" was just a wrapper around a UUID, with some internal GameObjectData methods attached to it, the gameobjects that the properties of newly moved objects referred to would just resolve to nothing in the new dungeon, instead of creating a concurrency nightmare.

There was no broadcast to all dungeons with that paradigm, and objects in different dungeons had to communicate with each other asynchronously with a rather obtuse task passing system.  That worked ok with a convenience layer on top of it, until the objects lost track of which dungeon the other object was in and sent the tasks to the wrong place.  I never did fix this, but thought about implementing some kind of Object ID registry in the realm so that things could look up what dungeon an object with some ID was in.  This would have put a crimp in the massively concurrent idea (though I suppose I could have stored it in some kind of big distributed hashtable), so I kept thinking about it and never got around to fixing it.

So, there it is.  Rather impractical for a game of roguelike scope, but it sure was fun figuring out how to make it work.  Lol.  Not designing it to be highly concurrent would have made it vastly cleaner.  A single-threaded game could have just kept direct references to other objects instead of internally referencing a data object by UUID, and could have done away with the task-passing system.
« Last Edit: August 18, 2012, 05:01:37 PM by Leaf »

Leaf

  • Rogueliker
  • ***
  • Posts: 64
  • Karma: +0/-0
    • View Profile
    • Email
Re: clean code
« Reply #26 on: August 18, 2012, 04:48:00 PM »
I also briefly explored using software transactional memories and a threadpool without the distinction between Dungeons, which would have allowed a really huge seamless world to run on multiple cores without having to deal with locking issues.  But they all still seem to be academic things that aren't quite ready to go into production code, yet....

I also tried implementing it all on J2EE with the whole game state living in google bigtable and memcache, but it ended up being just a little too slow to be nice.
« Last Edit: August 18, 2012, 04:50:02 PM by Leaf »

tuturto

  • Rogueliker
  • ***
  • Posts: 259
  • Karma: +0/-0
    • View Profile
    • pyherc
Re: clean code
« Reply #27 on: August 21, 2012, 07:25:18 AM »
Wow, that's a wall of text with loads of good information. Took while to read and digest it properly too.

Well, anything that is highly distributable and concurrent is bound to be more complex than single user application running in a single thread.

The use of properties is really nifty way of doing things. Like you said, adding new features on the existing system is easier that way, than when everything is in a huge blob of code. Writing the whole realm system must have been fun too.

How long did it take to get initial version up and running?
Everyone you will ever meet knows something you don't.
 - Bill Nye

tuturto

  • Rogueliker
  • ***
  • Posts: 259
  • Karma: +0/-0
    • View Profile
    • pyherc
Re: clean code
« Reply #28 on: August 21, 2012, 08:31:14 AM »
While we're talking about clean code, Gamasutra ran an article In-depth: Cleaning bad code. Nothing reaslly ground breaking I guess, but still interesting to read. Frykholm has very opionated way of writing, which is good for starting discussions.
Everyone you will ever meet knows something you don't.
 - Bill Nye

Krice

  • (Banned)
  • Rogueliker
  • ***
  • Posts: 2316
  • Karma: +0/-2
    • View Profile
    • Email
Re: clean code
« Reply #29 on: August 21, 2012, 09:20:30 AM »
Frykholm has very opionated way of writing, which is good for starting discussions.

That article was actually good and he knows what he is talking about. I think at least 3, 5, 6, 7 and 8 are confirmed.