Adding an Enemy in an Isometric World

We have created an isometric world and a character that moves around in the world.  But let’s say we want to add things in the world that the character can interact with.  There may be an enemy on another tile, for example, and when the character encounters this enemy, the game would switch from a “map mode” (which is where we left off in my previous post) to a “battle mode” where the character must fight the enemy.

Uh oh! It's that sheep again!

Uh oh! It’s that sheep again!

So let’s add an enemy model:

var EnemyModel = Backbone.Model.extend({
   defaults: {
      x : 0,
      y : 0,
      row : 0,
      column : 0,
      destX : 0,
      destY : 0,
      destRow : 0,
      destColumn : 0
   }
});

And let’s add the enemy on tile (3,3) by editing MapView:

/**
 * Load enemy
 */
loadEnemy : function(context) {
   var img = new Image(),
   originTile = context.tileMap[3][3];
   img.src = 'img/enemy.png';
   $(img).load(function() {
      // create spritesheet and assign the associated data.
      var spriteSheet = new createjs.SpriteSheet({
         //image to use
         images: [img],
         //width, height & registration point of each sprite
        frames: { width: 152, height: 156, regX: 0, regY: 0 }
      });
      context.enemy = new createjs.BitmapAnimation(spriteSheet);
      context.enemy.x = originTile.get('x');
      context.enemy.y = originTile.get('y');
      context.enemyModel = new EnemyModel({x:context.enemy.x, y:context.enemy.y, row: originTile.get('row'), column: originTile.get('column') });
      context.enemy.regX = 110;
      context.enemy.regY = 174;
      context.enemy.currentFrame = 0;
      context.stage.addChild(context.enemy);
      context.stage.update();
      Main.init();
      context.trigger('EVENT_MAP_LOADED');
   });
}

Notice that it triggers the event “EVENT_MAP_LOADED” at the very end.  I have made loadEnemy be the last thing loaded, after the world and after the player have been loaded, so that the game will be notified that everything in the world have been loaded.  We will go over what happens when this gets triggered later.

Modify movePlayer() in MapView (see code in red):

...
if ( (playerX === destX) && (playerY === destY) ) {
   //console.log('splice');
   context.model.set('movePlayer', false);
   context.playerModel.set('row', destRow);
   context.playerModel.set('column', destColumn);
   context.playerModel.set('x', destX);
   context.playerModel.set('y', destY);
   if (context.path.length > 0) {
      context.path.splice(0,1);
      if (context.path.length > 0) {
         context.movePlayerToTile(context, context.path[0]);
      } else {
         context.model.set('movePlayer', false);
         if ( (context.playerModel.get('destRow') === context.enemyModel.get('row')) && (context.playerModel.get('destColumn') === context.enemyModel.get('column')) ) {
            context.trigger('EVENT_MAP_REACHED_ENEMY');
         }
      }
   } else {
      context.model.set('movePlayer', false);
       if ( (context.playerModel.get('destRow') === context.enemyModel.get('row')) && (context.playerModel.get('destColumn') === context.enemyModel.get('column')) ) {
          context.trigger('EVENT_MAP_REACHED_ENEMY');
       }
   }
}
...

You will notice another event here, “EVENT_MAP_REACHED_ENEMY.”  When the player and enemy are on the same tile, this event will be triggered.  So what happens when this gets triggered, and when the other event, “EVENT_MAP_LOADED” gets triggered?  Let’s go to our main file to find out.  I’ve highlighted those parts in red, as well as some other parts that get affected.

var Main = (function($){
   var mapModel = new MapModel(),
   mapView = new MapView( { model: mapModel } ),
   gameMode = 'MODE_MAP';
   var init = function() {
      mapView.on('EVENT_MAP_LOADED', Main.onLoaded);
      mapView.on('EVENT_MAP_REACHED_ENEMY', Main.onReachedEnemy);
   };
   var onLoaded = function() {
      Main.ticker();
   };
   var onReachedEnemy = function() {
      alert('BAAAAAHTTLE MODE!');
      Main.gameMode = 'MODE_BATTLE';
   };
   var ticker = function() {
      createjs.Ticker.setFPS(40);
      createjs.Ticker.useRAF = true;
      createjs.Ticker.addListener(Main); // look for "tick" function in Main
   };
   var tick = function(dt, paused) {
      switch(gameMode) {
         case 'MODE_MAP':
            if (mapModel.get('movePlayer') === true) {
               mapView.movePlayer(mapView);
            }
            break;
         case 'MODE_BATTLE':
            break;
      }
      mapView.stage.update();
   };
   return {
      gameMode : gameMode,
      init : init,
      onLoaded : onLoaded,
      onReachedEnemy : onReachedEnemy,
      ticker : ticker,
      tick : tick
   };
})(jQuery);

So, on “EVENT_MAP_LOADED,” we start the ticker for the game loop.  “tick()” gets called every frame.  In tick(), we have a switch case with different game modes.  Initially, we are in “MODE_MAP,” which is the mode in which the character travels around in the isometric world.  When the character reaches the enemy’s tile, “EVENT_MAP_REACHED_ENEMY” gets triggered, and we switch to “MODE_BATTLE.”  At this point, the character is not able to move around in the world anymore, and we can switch to a different screen that can perhaps show the character’s hit points, skills, items, etc. for fighting the enemy.  The interactivity at this point can be real-time combat or turn-based or a mixture of both… it’s up to you!

BAAAAAHTTLE MODE!

BAAAAAHTTLE MODE!

Of course, this concept can be followed when adding items to the world (reaching a tile with an item on it could trigger a prompt to pick up the item) or people (reaching a tile with a person on it could trigger dialogue), and we could have different game modes for each of these.

Ideally, we would want to make a state machine to handle the different game modes, and the different modes can be in a data structure, such as a stack.  We could be in MODE_MAP when travelling around the world, and when we reach an enemy, push MODE_BATTLE to the stack, and when done, pop it off.  Eventually, we could have many different modes, such as MODE_INVENTORY to manage your items or MODE_SETTINGS to update your game settings.  But since we are in a “prototype” phase, we can go with this way for now.

Speaking of prototype, I may switch to another framework as I realize more and more of this game’s requirements for its production phase, such as mobile support.  I may write about this more in my future blogs, so stay tuned.

Creating a Character in an Isometric World

In my previous posts, I have written about how to create an isometric world and how to move a character to tiles using pathfinding.  However, the character I used did not have the proper angles for an isometric world.  So today will be somewhat of a more art-focused post on how I created a character for this.  Also, I don’t claim to be an artist, I just doodle really, so any feedback would be greatly appreciated!

I started off with a front-facing character sketch (using pencil and paper) so that I have a basic idea of the design I wanted to go for.  Mainly, my inspirations were anime, chibi, and steampunk:

Image

From there, again in pencil and paper, I sketched the isometric front and back of the character.  I made this version more chibi-like with more defined lines.  That way, when I scan it, it’ll be easier to see and trace:

ImageImage

Then, I scanned the isometric front and back sketches and traced them in Flash.  I chose Flash because I already have the program, and it keeps the image in vector format so that it’s easy to scale up and down without downgrading the image quality.  Some other options for drawing would be Illustrator, Photoshop, Manga Studio, Sketchbook Pro.  Any others?  Also, I used a really old and small Wacom Graphire4 tablet.  Another option would be a Cintiq.  Or a mouse.  Hehe.Image

As you can see in the screenshot above, I tried to separate the different body parts and clothing into different layers.  This makes it easier to make changes later.

Image

I didn’t trace it exactly.  I wanted to shorten her legs a little.  If you want to make some changes in your design, now’s a great time to do so.  Again, separating the body parts into different layers makes this process much easier, especially if you make mistakes and end up erasing a lot.  I also made separate layers for the colors.  So underneath the “hair” ink layer, I had a “hair color” layer, and so on.  I think it’s also important to label the layers and folders for organizational purposes, and it’ll be easier to remember what you did when you go back to it later on.

Image

Also make sure that you match it up with the tile you’re using!

I am using only four directions in my pathfinding, so I only needed to ink and color the front and back to make the first two directions, and then do a “Modify – Transform – Flip Vertical” to do the last two directions, since they are really mirrors of each other.

Image

The above image would make a great spritesheet (without the directions text of course), and the character dimensions were about 102×195 each.  Using EaselJS, I applied this into the spritesheet settings in the code.  My loadPlayer() code looks something like this:

// create spritesheet and assign the associated data.
var spriteSheet = new createjs.SpriteSheet({
  //image to use
  images: [img],
  //width, height & registration point of each sprite
  frames: { width: 102, height: 195, regX: 32.5, regY: 16.25 }
});

You would then need to associate the four character frames with the four directions.  For example, if the character is moving to a higher row (like from row 0 to row 2), but remains in the same column, she is moving south.  In the spritesheet, it’s the first image, so it’s frame 0.

if ( row < destRow && column === destColumn ) {
  context.player.currentFrame = 0;  // south
} else if ( row > destRow && column === destColumn ) {
  context.player.currentFrame = 2;  // west
} else if ( row === destRow && column < destColumn ) {
  context.player.currentFrame = 3;  // north
} else if ( row === destRow && column > destColumn ) {
  context.player.currentFrame = 1;  // east
}

To view the working example, check it out here!