Wednesday, November 23, 2011

Towards full Component Design

I was wrong.


Or shortsighted, or optimistic, or simply inexperienced.  In any case, we're working towards a more complete component-based system for our game objects.


If you recall the post from a few weeks ago, I was of the opinion that game objects could be implemented in terms of multiple inheritance.  This was good because it avoided the added complexity of managing various component objects.  What we didn't foresee was that this actually made things more complicated in certain cases.


Here's a really simple one, to illustrate.  Let's say we're creating a unit similar to the StarCraft II Viking.  The Viking is a unit whose distinguishing feature is that it is an aircraft capable of transforming into a ground-roaming mech.  As such, it must incorporate code to handle movement in the air and movement on the ground.


At first glance this seems not to cause any problems.  Being naive, we go ahead and have class Viking_clone inherit from class Walker and class Flyer, but now we have a problem!  Which move() function gets called?


The Viking from StarcCraft II
One solution is to name them separately -- perhaps fly() and walk() -- but that hasn't really solved anything.  All we've done is spread the problem out in the codebase.  Now, anytime we have code that tells a game object to move, it has to check which type of object it is and call the corresponding method.  To make matters worse, if we later decide to add another type of unit, say a boat, we have to edit code throughout the project to accommodate this new type of object.


On a more formal level, what we've done is violate the golden rule of game design:  keep things separate.  Module A should not care about how module B's internal structure.  It should instead interact with a constant and stable interface.


To get back to the Viking Problem, one solution is to implement a metaclass such as this:
class vikingClone:
    def __init__(self):
        self.vclone_air = vclone_air()
        self.vclone_grnd = vlcone_grnd()
        self.active = self.vclone_air
        
    def go(self):
        self.active.move()
        self.syncAG()
        
    def syncAG:
        ...

All we've done is create two separate classes, Viking_clone_air and Viking_clone_ground, each with its own methods.  To move the unit, the one would just call the appropriate method from the metaclass, which in turn calls the corresponding method from self.active.


From there, it's just a matter of calling a function that updates the non-active version of the Viking object.


If you're following my logic, you can see how this is only a short step away from full-blown component management.  To be sure, this would be a great refactoring solution if we had already coded up the vast majority of unit-related stuff, but it's still hackish and wasteful.  We don't need two copies of the Viking clone's health or attack function, for example.


What we're doing instead is managing all aspects of the object's behavior by assigning component objects.  The metaclass will only house the information which is completely unique to the unit:  health, for example.


Also, this potentially allows us to save memory by destroying the unused movement-type object:  if the Viking clone is walking, we destroy the instance of class Flyer.  Of course, this has to be weighed against the cost of initializing a new object every time the unit changes configuration or the complexity of using a pool of objects, but the point is we now have options.


So there it is, I stand corrected.  The good news is that the problem became apparent pretty early on, so we haven't wasted too much time.  The whole process of implementing components has been relatively pain free as well.



I'm currently working on an input management class along with the ground movement class.  The latter is nearly done!

Monday, November 7, 2011

Fallout from the Reddit Discussion

A few days ago I posted an overview of a discussion I had on reddit.  The question was simple:  what's wrong with RTS games and what can we do to fix it?
There were many long and well thought-out comments, which boil down to two things.
  1. Many people like the design principles of Project Orbit
  2. People are finding it difficult to get an exact idea of what our gamplay will be like.
This second point is entirely our fault.  It's hard to build on shifting ground, and we've spent a lot of time criticizing games and considering alternatives rather than explaining exactly what we plan to do.

Let's fix that, Q & A style.


Q:  What is the main design goal of project orbit?

A:  The main goal is to make a real-time strategy game that prioritizes informed, intelligent, cooperative and creative tactical thinking.

Q:  That sounds like much ado about nothing.  What does that mean?

A:  Those words are carefully selected.  Let's review them one by one.

Informed means that we want players to view battlefield intelligence as a requisite resource.  In many RTS games, the ability to see what your enemy is doing before making contact is a perk -- something you don't need, but is still useful.  Players spend most of their time in ignorance and seclusion.  We want our players to almost always be in some sort of precarious contact, constantly scoping each other out, circling in the ring, looking for weakness...

Intelligent means that the consequence of actions should vary with context.  The correct thing to do in case A is not necessarily the correct thing to do in case B.  Players should have analyze a situation and the environment in which it takes place.

Cooperative means that there should be a clear advantage to working with an ally.  This implies more than simply allowing cooperation or even encouraging it informally.  Team that cooperates should have an enormous advantage over the one that does not, and this advantage should be part of the game's fundamental design.  It also means that we will have to give the player tools and an interface to make cooperation fluid and natural.  This interface design will be a major focus of Project Orbit.

Creative means that several solutions should exist for a given problem.  This prevents an opponent from being predictable.  Unpredictable opponents imply intelligence gathering...

Q:  How will these design goals be implemented?

A:  Aha!  Now you want a feature list.  So be it, but bear in mind that these may still change in their details.
Informed Gameplay:
  • Large maps with relatively small unit caps to force exploration and patrolling
  • Terrain-sensitive radar with blind spots and other limitations
  • Specialized and limited static defenses to promote specialized and planned attacks
  • Need for resupply on many important units to encourage players to seek out and destroy enemy forward outposts.
Intelligent Gameplay:
  • Complex unit interactions to promote varied, but planned squad composition
  • Squad level tactics
  • Specialization bonuses in the form of a tech tree that is unlocked from XP earned in game.  This will encourage use of specialized tactics and unit.
  • Modern interface to facilitate micromanagement and maximize understanding of unit interactions.  In other words, a low click-per-second rate.
Cooperative Gameplay:
  • Shared tactical map interface with planning overlay to draft plans
  • VOIP communication tools
  • Specialization bonuses encourage players to select complementary roles
  • Economic and XP bonuses for cooperative play
  • Complex semi-scriptable interactions between friendly patrols, i.e. killboxes, artillery spotting and combat air patrols.
Creative Gameplay:
  • Micromanagement interface will abstract away excessive clicking.  This will free cognitive resources for planning and reaction
  • Contextual sensitivity of units will be clear and predictable to allow for valid reasoning
  • Communication and planning interface will allow for sharing of wisdom and experience
  • Rock-Paper-Scissors organization of unit types will allow for fine-tuned tactics.

Updated Caer Model: Head

Brian pushed his latest update for the caer concept onto our shared dropbox folder a few days ago.  It's really shaping up!


We still need to tweak the posture a bit, and probably thoracic proportions also.  For now we've actually decided to work on something else, because we don't want to overwork this model and end up killing it with our zeal.  Sometimes it's best to just let things marinate.



In any case, share your thoughts in the comments and stay tuned for the next model.

Saturday, November 5, 2011

Components in Python (or, an ode to Simplicity)

It seems as though blogger Mick West's Evolve your Hierarchy article has become something of a reference in game development circles.


The principle advantage was immediately clear to me -- less code duplication.  Whether this is the best way of going about reducing code duplication is much less clear.  Design patterns are nice because they format your thinking and so help you keep ideas clear and concise.  Design patterns are also evil because they're a tempting default solution to a problem that may have subtle, but significant peculiarities.


I've been thinking of a number of approaches that implement strict component design in varying capacities.


One approach is to implement a system of shared component objects.    For example, a tank object would only contain member variables pertaining to its internal state (health, armor, speed, etc.).  The functionality would come from previously-initialized component objects that would have no access to internal state attributes.  These objects would serve only as offsite number crunchers.


To illustrate, consider this example:
class Tank:
    def __init__(self, groundMove, takeInput, groundAttack):
        # Internal State Attributes
        self.health = 30
        self.damage = 10
        self.speed = 10
        
        self.dest = None
        self.target = None
        self.current_location
        
        # Component attributes
        self.move = groundmove    # These objects have been previously initialized
        self.inpt = takeInput
        self.attk = groundAttack
        
    def moveTo(self):
        self.move.goto(self.dest, self.speed)
        
    def attack(self):
        self.attk(self.target, self.current_location, self.damage)

Note that in this example, member variables are passed into the component objects individually.  This is simply illustrative.  In reality, it is much simpler to pass a pointer to the entire self object.


This approach has the advantage of being memory-efficient.  There is one object for each component, and units are implemented as objects that issue calls to component-object member functions.  This is great but it has two drawbacks:

  • Complexity
  • Bad for concurrency
We expect to have to resort to concurrent programming at some point.  I'm honestly thrilled at the idea of learning more about it, but it's still tricky business.  Code like this doesn't make it any easier.

The obvious solution is to have a pool of component objects and to select one that isn't being used, but this sounds like an unholy mess of deadlocks.  And besides, we don't need the concurrent processing yet, so why bother implementing it before it's needed?  


In fact, do we even need the memory we're saving?  Probably not, at least for the time being.  The concurrency problem can thus be solved with a simple change:
# Component attributes
        self.move = groundMover()
        self.inpt = Playable()
        self.attk = groundAttacker()

All we do here is call the constructor of the relevant components.  Our tank now has its own component object, so no more concurrency problems.


This bothers me because we've added a huge layer of complexity when multiple inheritance can also fix this problem.  I'm not an expert in python, so I'm not entirely sure of the potential drawbacks of using multiple inheritance in terms of performance, but here's what I do know...


"Impulses to optimize are usually premature"

Jonathan Blow, lead developer of Braid, gave an excellent talk on the art of getting things done.  One of the things that struck me was his stern warning against premature optimization.  "Impulses to optimize," he says, "are usually premature."

It makes sense.  Most optimizations make assumptions that could be invalidated later in development.  More importantly, the code being optimized usually has a negligible effect on the overall experience -- people optimize the wrong things.

This approach makes a lot more sense to me:
class Tank(groundMover, pcUnit, groundAttacker):
    def __init__(self, groundMove, takeInput, groundAttack):
        # Internal State Attributes
        self.health = 30
        self.damage = 10
        self.speed = 10

Boom.  Simple.  For one, there's a lot less code to write, which translates into fewer bugs.  Second, concurrency is fairly straightforward as all units are (mostly) independent.  Third it isn't optimized, so it's more likely to work with a novel case.


For the price of a bit of free ram, we have something that's much more elegant and simple to understand.  That said, I'm not very familiar with the workings of python inheritance, so perhaps this is doing something insanely suboptimal behind the scenes.  But then again... who cares?


If it doesn't work, I'll change it, but until that day comes, I'll go with the simple option.

Friday, November 4, 2011

Tension Rises

Commander’s Log (December 26, E3032)

Interception has been confirmed. Initial theories involve rogue Work-Class engineers working on unregistered terminals. The IS is insisting on a thorough investigation and complete secrecy, while the Work-Class cries ever louder for transparency to remain, as promised. Though I agree they should be informed, I have no authority over the IS. As the months go on, however, it’s become harder to take protocol seriously.

3 years 101 days.


Commander’s Log (December 28, E3032)

Paranoia is spreading. With transparency compromised, the Work-Class is left to rumors and speculation. They seem to have reached the conclusion that no one in the IS will even acknowledge as a possibility. Numerous Work-Class units have begun scanning for UCBs, many more have attempted unauthorized access to the IS network resulting in IS retaliation. Unless an answer is found soon or the IS re-installs transparency, Terminus will be at risk of a level of disorder that the colony cannot survive.

3 years 103 days.


As mentioned before, humans have never encountered an alien race in all of their exploration of the Milky Way, and as such, the thought of ever finding them has diminished into almost a joke. This is why what's happening to Commander Brewer a turning point, and why he, as well as other leaders, remain skeptical, as well as why paranoia and fear are spreading like wildfire and why Commander Brewer is so desperate solve it.

In my next post, I will explain why Terminus is in such a desperate position.

What's Missing from RTS Games?

I managed to work through a couple of animation bugs this evening.  The odd thing about a bug-squashing streak is that it makes the next frustrating bug much, much more frustrating.


Instead of banging my head against the only QWERTY keyboard in all of France and running the risk of having to type on the abomination that is the AZERTY keyboard, I decided to do some brainstorming on the GameDev subreddit.  The result is this post where I ask the following question:  what are your biggest gripes in RTS gaming?


I won't lie, I was sort of hoping to be praised as a visionary, but the tough love I got from a few redditors was both useful and insightful.  The following points, in no particular order, were made:
  • Build orders are not the enemy.  Don't hate them.
  • Unit responsiveness is of capital importance
  • It sucks when RTS games go on and on forever
  • On the other hand, it sucks when you win by running the clock
  • Don't use units as cannon fodder
  • UIs need some serious innovation
  • Cover systems suck
The last one was quite an eye opener.  I had actually enjoyed the cover system in Company of Heros, but the point was made that it restricted a player's use of the terrain.  Since Project Orbit will rely heavily on terrain influence, cover systems may be more of a bug than a feature.

Food for thought, as they say...

Friday, October 28, 2011

Unveiling the Project Orbit Logo

Redditor Agentz101 was kind enough to design a banner for Project Orbit.  He didn't ask for anything in return, but if you feel so inclined, you can do what I did -- systematically upvote all his comments.


Thanks Agentz101!