Gamieon is a one-developer part-time studio focused on creating video games for the desktop and mobile platforms. Since 2010 Gamieon has developed and released six games as well as six more prototypes. Including beta distributions, Gamieon's products have netted a combined 300,000 downloads! Below this bio are images from all my games and prototypes. Check them out and don't forget to follow me on Twitter @[Gamieon](members:gamieon:321769)

Report RSS How I programmed a Virtual Hamster Cage

Posted by on

Click here to watch the hammys in your web browser!


Introduction

One bonus feature in Hamster Chase, coming soon for Android and iOS is the "virtual hamster cage" which allows players to see and interact with four different hamsters who are just playing around. Having owned close to a dozen hammys over the course of my life, the implementation is based on a design influenced by my own experiences with them: Their behavior was unpredictable when in their cage, and each one had different tendencies. For example: Of my last two hamsters "Patty" and "Selma," one spent every waking moment trying to escape, and the other would watch me from the same cozy spot while I was using my computer. Of course, both were quite happy when I took them out of their cage, gave them treats, and let them run around and play while I made sure they didn't escape...more than usual.

Hamster Behaviors

In Hamster Chase, a single hamster can smile, run on their wheel, fall off their wheel, look left and right, play with a ball, play hide-and-seek, sleep, eat, wash, or just stand idle. The vastly oversimplified version of the algorithm I use is:

while (true) { do_behavior( get_mostly_random_behavior() ); }

Here are the rules of the algorithm:

  • A "behavior" is simply a series of animations and screen movements of the hamster.
  • Each time a behavior is executed, its duration should vary. This adds to the apparent randomness of the hamsters.
  • Hamsters need their boundaries; they can't run outside the visible cage or float up in the air.
  • You can't go from just any behavior to any other behavior. For example, it makes no sense for a hamster to be sleeping, and then immediately fly off its wheel.
  • For added fun and realism, each hamster should have its own tendencies. Goldie, for instance, is the biggest hamster and loves to eat. So, Goldie should be eating more than the other hamsters on average.
  • For additional fun, the player should be able to tap on the screen to encourage certain hamster behaviors based on where they tap.

So what kind of implementation satisfies these rules? I chose one that consists of each hamster having its own behaviors and tendencies expressed as a graph, and a state machine that controls the hamster using that graph as a road map for each hamster.

The "Hammygraph"

The behavioral patterns of a hamster are expressed in a graph called a "hammygraph." Think of every hamster's behavioral pattern expressed as a tree as shown below:

The life of a virtual hamster is expressed as the random traversal of this graph. At any given time, one and only one node in the tree is "active," that is, what he hamster is currently doing. After a random amount of time elapses, the state machine will randomly decide to either change the active node to its parent node, or to a random one of its children, or not change it at all.

Here is one example traversal of a hamster who begins asleep, wakes up, then washes, and then smiles.

There are some rare exceptions: When the active node is one of the hamster flying off its wheel, the active node will always go back to the root "standing idle" node because the hamster should neither repeat its fall nor be placed back on the wheel. Another example is when the user taps on the cage. Depending on where the tap occurred, the state machine will figure out which node the user is trying to encourage action in, and temporarily give it a much increased chance of becoming active. For instance, if you repeatedly tap on the wheel, eventually all four hamsters will be running on it.

Here is part of Sasha's hammygraph expressed as XML:

xml code:
  <Behavior name="StandingIdle" weight="0.8">
    <GoingTo>    
    </GoingTo>
    <Resident>
      <Movement hamsterMode="IdleGround" x1="-4.8" y1="-2.60" relz1="0" x2="4.8" y2="-2.60" relz2="0" />
      <Animation hamsterMode="IdleUpright" type="Simple" frameTransitionTime="0.2" pauseTime="0">
        <AnimTextureArray name="texIdle" />
      </Animation>
      <RandomDelay min="0" max="1.5" />
      <HamsterCommand command="Blink" />
      <Animation type="Simple" frameTransitionTime="0.3" pauseTime="0">
        <AnimTextureArray name="texIdle" />
      </Animation>
      <RandomDelay min="0" max="1.75" />
    </Resident>
    <Leaving>    
    </Leaving>

    <Behavior name="Wheel" weight="1.0">
      <GoingTo>
        <Movement hamsterMode="IdleGround" x1="1.35" y1="-2.60" relz1="0" x2="1.65" y2="-2.60" relz2="0" />
        <Leap x1="2.811295" y1="-1.757367" relz1="-2.0" h1="0.75" x2="2.811295" y2="-1.757367" relz2="-2.0" h2="0.75" />
        <ReserveWheel />
      </GoingTo>
      <Resident>
        <WheelRun min="2.0" max="6.0" />
      </Resident>
      <Leaving>
        <ReleaseWheel />
        <Leap hamsterMode="IdleGround" x1="1.35" y1="-2.60" h1="0.75" relz1="0" x2="1.65" y2="-2.60" relz2="0" h2="0.75" />
      </Leaving>
     
      <Behavior name="WheelSpinAndThrow" weight="1.0" ignorePreviousLeavingInstruction="true" >
        <GoingTo>
        </GoingTo>
        <Resident>
          <WheelSpin hamsterMode="IdleGround" min="6" max="10" />
          <ReleaseWheel />
          <StartNextBehavior name="/HamsterBehaviorGraph/Behavior[@name='GettingAttention']" />
        </Resident>
        <Leaving>
        </Leaving>
      </Behavior>
     
    </Behavior>

    <Behavior name="BallPlay" weight="1.0">
      <GoingTo>
        <ReserveBall />
      </GoingTo>
      <Resident>
        <BeginMoveBall x1="-2.8" x2="2.8" />
        <RandomDelay min="0" max="2.0" />
      </Resident>
      <Leaving>
        <ReleaseBall />  
      </Leaving>
    </Behavior>

There are various kinds of nodes; each with a unique purpose:

Behavior Nodes: These nodes define all the different hamster behaviors

  • Behavior: This node contains everything there is to know about a single hamster behavior. Its children and grandchildren represent instructions that the state machine interprets in order of appearance.
  • name attribute: The name of the behavior
  • weight attribute: The tendency of the hamster to engage in the behavior. This is explained later in the article.

Behavior Child Nodes: These three nodes describe the life cycle of a behavior node. These are all children of behavior nodes.

  • GoingTo: Tells the program what series of actions to do when the behavior becomes active.
  • Resident: Ttells the program what series of actions to do after the GoingTo animations have been completed. These actions may be repeated if the state machine decides not to change the active node after the current beahvior time expires.
  • Leaving: Tells the program what series of actions to do when the behavior becomes inactive. This happens before the next node's GoingTo actions take place.

Action Nodes: These nodes describe the instructions for the program to follow in each part of a behavior child node.

  • Movement: Tells the state engine to make the hamster crawl to a position within a given range. Animating the crawl is inferred.
  • Animation: Tells the state engine to perform an animation. I won't detail its attributes and child nodes in this article; it's too much ground to cover here.
  • RandomDelay: Tells the state engine to just wait for a random period of time.
  • HamsterCommand: Tells the state engine to do a special action that is just easier to maintain when completely done in code.
  • StartNextBehavior: Forces the state engine to jump to another behavior that doesn't follow the usual traversal rule.

There are other nodes used for custom purposes as well; including ReserveBall, ReleaseBall, Leap, and WheelRun which I won't detail here.

Interpreting the Hammygraph

Each hamster has their own instance of the controlling state machine, and a FIFO queue of Action Nodes (Movement, Animation...). At the front of the queue is the current action being executed. Given all this new information, I can now give you a more accurate picture of the main loop:

code:
while (true)
{
  if (finished_with_current_action)
  {
    if (action_node_queue.count > 1) {
      action_node_queue.dequeue();
    } else {
      build_behavior_candidate_list(action_node_queue.head);
      action_node_queue.dequeue();
      choose_behavior_from_candidate_list();
      if (chosen_behavior == current_behavior) {
        action_node_queue.enqueue(current_behavior.Resident.action_nodes);
      } else {
        action_node_queue.enqueue(current_behavior.Leaving.action_nodes);
        action_node_queue.enqueue(chosen_behavior.GoingTo.action_nodes);
        action_node_queue.enqueue(chosen_behavior.Resident.action_nodes);
        current_behavior = chosen_behavior;
      }
      current_behavior.weight = max(0, current_behavior.weight - a_small_number);
    }
    execute_action(action_node_queue.head);
  }
}

In a nutroll: At each frame, the state machine checks whether the hamster is in the middle of an action; like running, washing, or eating. If not, then no further work is needed. Otherwise we need to dequeue the head node. If the queue is not empty, then the state machine invokes whatever functionality the new head action node calls for. If the queue is empty, then it's time to find a new behavior to go to.

"build_behavior_candidate_list" is where the state machine creates a list of all the behaviors that are neighbors of the active behavior node, as well as the active behavior node itself. Whatever the hamster does next is going to be something in that list. Every node in the list has a weight. The chance of that node becoming active next is equal to its weight divided by the combined weights of all items in the list. To minimize repetition, the weight of the active behavior itself is decreased each time it "wins the behavior lottery" until it goes down to zero and cannot be selected. When the active behavior loses its active status, its weight resets back to its original value.

Once the candidate list is created, the random node is chosen from that list to become active. At that point, its weight is decreased, the formerly active node's weight is reset, and then the enqueueing beings. The Leaving actions of the formerly active node are enqueued first, followed by the GoingTo actions of the new active node, and then finally the Resident actions of the new active node.

After all that enqueueing is done, the state machine gets to work following the instructions of the head action node.

Tapping on the Cage

The act of tapping on the cage creates an added effect in the state machine:

code:
while (true)
{
  if (user_taps_cage) {
    b = get_behavior_corresponding_to_tap(tap_position);
    b.weight += tap_weight;
    encourage_early_behavior_completion();
  }
 
  do_for_all_behaviors {
    if (behavior.weight > b.minimum_weight) { behavior.weight -= weight_pull; }
  }
}

What this means is whenever you tap on a part of the cage, the program will map the touch location to a behavior. For example, tapping on the wheel will tell the state machine that you want the hamsters to perform the Wheel behavior. Tapping on the hamster seeds tells it you want the Eating behavior.

Once the mapping is done, the program will increase the tapped behavior's weight by a value which we'll call "tap_weight." That alone is not enough to drive the hamster to do what the user wants to do; so the "encourage_early_behavior_completion" function is called to invoke a small chance of the program immediately ceasing the current action and percolating to the next behavior...but only if the current behavior child node is "Resident."

In all cases, the program will always reduce these "user-encouraged" weights down to nothing so that the hamsters go back to their normal behavioral patterns some time after the tapping has ceased.


Putting it all together

When you put it all together, you get four happy hammys in a reasonably maintainable code base. Want to try it yourself? Visit my website and play with the hamsters yourself!


Check out my homepage and news feeds

And my projects!

Post a comment

Your comment will be anonymous unless you join the community. Or sign in with your social account: