(7/8/2013 - This Unit is still a work-in-progress - There's missing notes on transformations and explanations on the code
Overview
This is a fun Unit, as you'll really start seeing the fruits of your hard work! Expect it to take about 3 hours, depending on your comfort with Linear Algebra. To demonstrate what you should have at the end of this Unit, check out the Demo for Unit 9.
Two big things we're going to talk about this Unit are Sprites and Sprite Sheets. This will be a big setup going into building an Animation system in the next Unit.
Sprites are essentially GameObjects, but have the ability to be drawn. We are going to have some math-y stuff in order to deal with transformations (position, rotation, and scale), as well as getting more comfortable with the Canvas API, otherwise are very straightforward and nearly the same to the Test Code and demo from the previous Unit.
A Sprite Sheet is an important feature that should solidify our comfortability with build layers of encapsulation. It is a single texture that can represent lots of smaller images, and we want to support it for many good reasons, but primarily it's a lot easier and faster if we get one larger image from the server rather than lots of little images. The game loads faster and your teammates and players are both happy.
The three sections of this Unit are:
- Sprite Game Object class
- Sprite Sheet class
- Engine extension to manage Sprite Sheets
Sprite GameObject class
Sprite properties
For simplicity, all sprites are going to use a SpriteSheet whether there are sub-images in the image or not.
API Source Code:
//=========================================================================
// Sprite Class
// - Sprite Class is a GameObject, so it can have its own components
// (like an animation or physics) and bind to events.
// Plus it derives from DrawNode, so we'll implement the draw() function.
//=========================================================================
Engine.Sprite = Engine.GameObject.extend({
// Class name, for debugging
typename: "Sprite",
//=========================================================================
// Sprite Properties
//
defaults: {
x: 0, // position along the x-axis
y: 0, // position along the y-axis
angle: 0, // rotation in Radians
alpha: 1, // controls transparencty (between 0 and 1)
isVisible: true, // toggles visibility
scaleX: 1, // the scale along the x-axis (left/right)
scaleY: 1, // the scale along the y-axis (up/down)
sheetName: undefined, // the sprite name out of a loaded sprite sheet
frame: 0, // current frame in the Sprite Sheet
},
//=========================================================================
// Constructor function that takes in configuration propertiers
//
init: function(props) {
//@TODO - Implementation
},
//=========================================================================
// Draws the sprite on the canvas with it's current properties
// Can handle whether this sprite is using a single asset or SpriteSheet.
// Triggers a draw event in case components need to do any additional drawing.
//
draw: function(ctx) {
//@TODO - Implementation
}
});
init()
Implementation Source Code:
init: function(props) {
// base class constructor
this._super(props);
if( this.sheetName === undefined ) {
alert( "Cannot create a sprite that has no sheetName assigned to it" );
return;
}
// request the sprite sheet from the engine. If none was loaded, then it
// will create a new sprite sheet that falls back to using the entire image
this.spriteSheet = Engine.GetSingleton().getSpriteSheet(this.sheetName);
if( this.spriteSheet === undefined ) {
alert( this.spriteSheet, "Sprite could not find image by assetName: " + this.assetName );
return;
}
// if the user didn't supply width/height properties grab them from the asset
this.tilew = this.spriteSheet.tilew;
this.tileh = this.spriteSheet.tileh;
if(!this.width) {
this.width = this.tilew;
}
if(!this.height) {
this.height = this.tileh;
}
},
draw()
If you played around with your homework from the previous unit, you might have noticed that when you render an image it draws it so that the top-left corner of the image is at the position you provided to the canvas. Sometimes that can cause other behavior that you don't want - say you were to adjust the scale of the sprite, it would scale outward from the top-left corner but that corner will still be in the same position. Now if the sprite were centered around the position you meant to draw it at, then it would grow outward in all directions (and this is typically what most developers want).
Implementation Source Code:
draw: function(ctx) {
if( this.isVisible && this.spriteSheet) {
var width = this.width * this.scaleX;
var height = this.height * this.scaleY;
// save the current co-ordinate system before we screw with it
ctx.save();
ctx.globalAlpha = this.alpha;
// move to the middle of where we want to draw our image
ctx.translate( this.x, this.y );
// rotate around that point
ctx.rotate( this.angle );
// draw it up and to the left by half the width and height of the image
var sx = this.spriteSheet.frameX(this.frame); // where to start clipping
var sy = this.spriteSheet.frameY(this.frame); // where to start clipping
ctx.drawImage( this.spriteSheet.image, sx, sy,
this.tilew, this.tileh,
-(this.width/2), -(this.height/2),
this.width, this.height );
// and restore the co-ords to how they were when we began
ctx.restore();
}
// Let components know that a draw event occured, in case they need to do anything
this.triggerEvent('draw',ctx);
}
SpriteSheet class
There's not a lot to this class, as it's mostly a holder of data.
A SpriteSheet is not an Image. It is a wrapper of data, that knows about a set of sub-images within a larger image. A single image might have more than one Sprite Sheet associated with it!
This approach is also called a Texture Atlas, and there is a good amount of reading material available.
The real magic is in the data file (JSON) it gets associated with.
Implementation Source Code:
//=========================================================================
// Sprite Sheet Class
// A single set of like-sized frames of the same sprite.
// References an image asset with extra data about the frames, so that we
// can quickly draw a specific frame at an x/y location on the canvas.
//
// Requires a resource name (how sprites reference a SpriteSheet)
// and an image asset name.
// Other constructor options:
// tilew - tile width
// tileh - tile height
// width - width of the sprite block
// height - height of the sprite block
// sx - start x
// sy - start y
// cols - number of columns per row
//=========================================================================
Engine.SpriteSheet = Class.extend({
// Class name, for debugging
name: "SpriteSheet",
//=========================================================================
// Constructor function
init: function(assetName, options) {
this.engine = Engine.GetSingleton();
var imageAsset = this.engine.getAsset(assetName);
var defaultProperties = {
image: imageAsset,
assetName: assetName,
width: imageAsset.width,
height: imageAsset.height,
tilew: imageAsset.width,
tileh: imageAsset.height,
sx: 0,
sy: 0
};
_.extend(this, defaultProperties, options);
this.cols = this.cols || Math.floor(this.width / this.tilew);
},
//=========================================================================
// Calculates the frame x position within the image asset
frameX: function(frame) {
return (frame % this.cols) * this.tilew + this.sx;
},
//=========================================================================
// Calculates the frame y position within the image asset
frameY: function(frame) {
return Math.floor(frame / this.cols) * this.tileh + this.sy;
}
});
SpriteSheet management
Create new Sprite Sheets. Creating multiple sprite sheets from a single image and data file.
Accessing sprite sheets by name.
Implementation Source Code:
//=========================================================================
// Sprite Engine Module
// Centralized mechanism for compiling and tracking sheets to make
// them easy to reference and lookup.
//=========================================================================
Engine.defaults.spriteSheets = { };
//=========================================================================
// Creates a new sprite sheet
Engine.prototype.createSpriteSheet = function(name, imageAssetName, options) {
var asset = new Engine.SpriteSheet(imageAssetName, options);
this.spriteSheets[name] = asset;
return asset;
};
//=========================================================================
// Combines an image asset with a JSON sprite data asset to generate
// one or more SpriteSheets auomatically from the data generated
// by the spriter generator in Chapter 8.
Engine.prototype.compileSheets = function(imageAssetName, spriteDataAsset) {
var engine = this;
var data = engine.getAsset(spriteDataAsset);
engineAssert( data, "Cannot compile sprite sheets for missing image asset name: " + imageAssetName );
_(data).each(function(spriteData,name) {
engine.createSpriteSheet(name,imageAssetName,spriteData);
});
};
//=========================================================================
// Getter for a sprite sheet
// Lazy load mechanism, for images where the entire image is the sprite
Engine.prototype.getSpriteSheet = function(name) {
var asset = this.spriteSheets[name];
if( !asset ) {
return this.createSpriteSheet(name, name);
}
return this.spriteSheets[name];
};
Test Code
Source Code:
// Let's create a canvas object
var mainBGLayer = Engine.GetSingleton().createCanvas( "mainBGLayer", {
container: $('#gameContainer'),
backgroundColor: "#000",
width: 640,
height: 480,
} );
var assetList = { "playerSprite": "gamedata/Character Boy.png" };
var onFinishedCallback = function() {
var props = {
sheetName: "playerSprite"
};
var player = new Engine.Sprite( props );
mainBGLayer.attachChild( player );
};
Engine.GetSingleton().load( assetList, onFinishedCallback );
Homework
Part 1. Create and add sprites.js to your project, filling it out with the code from this Unit.
Part 2. In your test code, create a new sprite instance.
Part 3. Load a
- Create a canvas that gets added to the DOM (you may need to update your html - create a <div> and give it an 'id' to use)
- Create a new GameObject that will behave as a sprite
- Move the position of the object every frame. Try using Input events to trigger changes in the sprite's position.
Extra Credit
The SpriteSheet class makes an assumption: all frames have the same tile width and tile height. If we are going to say that a SpriteSheet has
Part 2. In your test code, create a new sprite instance.
Part 3. Load a
- Create a canvas that gets added to the DOM (you may need to update your html - create a <div> and give it an 'id' to use)
- Create a new GameObject that will behave as a sprite
- Move the position of the object every frame. Try using Input events to trigger changes in the sprite's position.