Skip to content

3. Multiple Sprites (Groups)

RossLote edited this page Jan 28, 2013 · 15 revisions

So much effort

The code we’ve written so far is absolutely fine if we only have a few sprites in our game but as our games become larger and more complex it will quickly become a huge coding task and could end up looking something like this:

window.onload = function(){
    CVSetCanvas(document.getElementById('canvas1'));

    var myImage = new CVSurface({
	width: 80,
	height:80
    });
    myImage.circle();

    mySprite1 = new MyBall({ image: myImage });
    mySprite2 = new MyBall({ image: myImage });
    // ... repeat for 3 - 49
    mySprite50 = new MyBall({ image: myImage });
    
    CVSetLoopFunction(function(){
	CVClearCanvas();
	mySprite1.update();
	mySprite2.update();
        // ... repeat for 3 - 49
	mySprite50.update();

	mySprite1.draw();
	mySprite2.draw();
        // ... repeat for 3 - 49
	mySprite50.draw();
    });
    CVInit();
    CVMainloop();
}

As you can see, it’s gonna be a bit of a pain in the ass. It is also extremely bad programming practice. The more experienced programmers amongst you will probably have already figured out that this can be simplified with arrays and for loops and you would be right. However, we’re way ahead of you. Introducing CVSpriteGroup. This is a built-in class that comes free of charge. You’re welcome! Let’s re-write the last bit of code to use CVSpriteGroup and after we’ll go over the changes.

window.onload = function(){
    CVSetCanvas(document.getElementById('canvas1'));

    var myImage = new CVSurface({
	width: 80,
	height:80
    });
    myImage.circle();
    mySpriteGroup = new CVSpriteGroup();
    for(var i = 0; i < 50; i++){
        mySpriteGroup.add( new MyBall({ image: myImage }) );
    }
    
    CVSetLoopFunction(function(){
	CVClearCanvas();
	mySpriteGroup.update();
	mySpriteGroup.draw();
    });
    CVInit();
    CVMainloop();
}
  • To start off we create an instance of the built-in CVSpriteGroup class and named it mySpriteGroup.
  • Next we create a for loop that will iterate 50 times. Each time the loop executes we call the add() method of our mySpriteGroup object. There are two options for this. You can create an instance of your sprite and assign it to a variable then pass the variable name in as an argument for add(). Or you can do it the way we have where you assign the object directly into a slot in the CVSpriteGroup object. Either way is perfectly acceptable and the decision really depends on how you need to use the sprites.
  • The last thing we did was to remove any sprites from our loop function and replaced them with our mySpriteGroup object calling update() and draw() respectively. These methods call update() and draw() on all sprites contained within the CVSpriteGroup. Check the documentation for more CVSpriteGroup methods.

Where are all the sprites?!

If you ran the code above you will undoubtedly have noticed a flaw in our code. There still only appears to be one sprite. Well this is because they are all being drawn in exactly the same place. Unacceptable! Lets fix it at once. Replace your for loop with this:

for(var i = 0; i < 50; i++){
    mySpriteGroup.add( new MyBall({
        image: myImage,
        dx: i/10,
        dy: i/10
    }) );
}

Before running the script you will also need to change MyBall’s initialize() method. Go ahead and remove this.dx = 1; and this.dy = 1; and now your script is ready to be run. You should new see a load of ball bouncing around your screen.

But we didn’t declare those attributes!

You may have noticed at this point that at no point in our code did we declare this.dx and this.dy yet in our calcPosition() method we clearly use them. Yet our code if working fine and we have no errors…”what’s going on here?”, I hear - well, sense - you ask. This introduces a new concept to object initialization. Any attribute you declare during the creation of an object will become an attribute of that object. Here’s how it works:

mySprite = new MyBall({
    image: myImage, // translates to this.image within initialize()
    dx: i/10,       // translates to this.dx within initialize()
    dy: i/10       // translates to this.dy within initialize()
});

So any key: value within the constructor will become this.key = value and can be used as an attribute from that point forward. I see a lot of the more experienced programmers cringing when they see that and it wouldn’t be completely without reason. But I have a few guidelines to put in place to prevent you encountering problems.

  • Be sure to declare any attributes you intend to use within initialize() with default values.
  • Never declare anything new when creating objects.
  • Always write your initialize() method in this format:
var MyClass = new Class({
    Extends: CVSprite,

    initialize: function(args){ // note 'args' (call this whatever you like)
        this.attr1 = defaultVal;
        this.attr2 = defaultVal;
        // ... etc
        this.parent(args); // note that this is at the end and passes in 'args'
    },
});
  • Always place at least one argument args, which represents the JSON object which is passed in, within the parentesis.
  • Always place this.parent(args) at the end of initialize() remembering to pass in args.
  • This only applies for classes that extend CVSprite and only for the initialize() method.

These are only guidelines remember and if you know what you are doing you can bend the rules slightly but just be aware that you code may break if you do.

Just for funzies

Now that we’ve gone over all of the basics lets just change some things in our code in order to demonstrate some additional functionality of CanVerse2D.

Before continuing, be sure to place some default values in your initialize() method if you already haven’t:

initialize: function(args){
    this.dx = 1;
    this.dy = 1;
    this.parent(args);
},

Now lets add some color. Find this line of code myImage.circle(); and replace it with this:

myImage.circle({
    fill: 'red',
    outline: 'blue',
    outlineWidth: 5,
    radius: 20
});

I won’t explain what all of the arguments do as I believe the naming makes it obvious. If you are interested in seeing what arguments can be passed in and what they do refer to the documentation. Just note that they are passed in JSON format and if you try any other way you will get an error.

Clickety Clack

Now lets add some interactivity. There are many built-in method that come with CVSprite all of which are give to you for free when you inherit from it. Lets make use of two of them, just to get our feet wet. Add this method to our MyBall class:

mouseDownEvent: function(evt){
    this.kill();
},

mouseDownEvent() is a method that is automatically called when a mouse button is pressed when over the sprite. There are other mouse methods available so check the documentation for more. kill() does almost exactly what it says on the tin. It actually only really works when used in conjunction with CVSpriteGroup objects. What it does is removes the sprite from any CVSpriteGroup that contains it. this means that it will no longer update or draw with the CVSpriteGroup. If there is no other reference to the sprite then it will be collected by Javascript’s Garbage Collection. There is one more thing you need to do before you can access the CVMouse objects functionality. Just before CVInit() place this line of code:

CVMouseEnabled = true;

This is false by default and needs to be true in order to use the mouse in your game. If you now run the script you will be able to have heaps of fun chasing balls round on the screen trying to kill them with clicks. The observant of you will notice a little problem with the rect when the sprite calls checkBounds() but we’ll fix that in the next lesson.

Done!

Here’s the complete code for you to see in all it’s glory:

var MyBall = new Class({
    Extends: CVSprite,

    initialize: function(args){
	this.dx = 0;
	this.dy = 0;
	this.parent(args);
    },

    update: function(){
	this.checkBounds();
	this.calcPosition();
    },

    calcPosition: function(){
	var pos = this.getPos();
	this.setPos(pos.x + this.dx, pos.y + this.dy);
    },

    checkBounds: function(){
	if(this.rect.left() < 0 || this.rect.right() > CVCanvas.width){
	    this.dx *= -1;
	}
	if(this.rect.top() < 0 || this.rect.bottom() > CVCanvas.height){
	    this.dy *= -1;
	}
    },

    mouseDownEvent: function(){
	this.kill();
    }
})

window.onload = function(){
    CVSetCanvas(document.getElementById('canvas1'));

    var myImage = new CVSurface({
	width: 80,
	height:80
    });
    myImage.circle({
	fill: 'red',
	outline: 'blue',
	outlineWidth: 5,
	radius: 20
    });
    mySpriteGroup = new CVSpriteGroup();
    for(var i = 0; i < 50; i++){
	mySpriteGroup.add( new MyBall({
	    image: myImage,
	    dx: i/10,
	    dy: i/10,
	}) );
    }
    
    CVSetLoopFunction(function(){
	CVClearCanvas();
	mySpriteGroup.update();
	mySpriteGroup.draw();
    });

    CVMouseEnabled = true;
    CVInit();
    CVMainloop();
}