GTCS Game Engine:
Tutorial 3: Sprites, Animation & Collision Detection

Tutorial 2 <-- Tutorial 3 --> Tutorial 4
Tutorial Homepage


Introduction

In this tutorial, we are going to look at another renderable type that implements animation of the image. We will also expand our understanding of game object behavior by using collision detection to determine when GameObjects overlap.

Covered Topics: SpritesAnimationCollision Detection

Demonstrations: Using a Sprite SheetAnimated SpritesDetecting Collisions

Complete source code for all tutorials can be downloaded from here.


Sprites

TextureRenderables have a big advantage over Renderables in that they are able to render a bitmap. However, they do have a few significant limitations. Our texture must have dimensions that are powers of 2 and they are static. While this may work fine for backgrounds or static visual elements, we typically want something more dynamic the enhance the "action" of the graphics in our game. A SpriteRenderable allows us to use a sprite sheet image and define the portion of the sheet we want to display for the renderable.

figure 3-1: Sprite Sheet

With this, we can store many if not all of our graphics in a single resource. In Figure 3-1, we see a sample sprite sheet. You will notice that the top two rows have variations of the same image and in the bottom row (second from the left), we have the same image we used in Tutorial 2 and a reverse facing copy in the far right. We can use these types of images to simulate motion and infer direction. Figure 3-2 provides more details about our sprite sheet.

Figure 3-2: Sprite Sheet in detail

With coordinate location (0,0) being the bottom left of the entire sprite sheet image, the bottom left coordinate of our minion texture from Tutorial 2 is at (130,0). The size of this sub-image is 180 pixels wide by 180 pixels tall. Using the call setElementPixelPositions(left-x, right-x, lower-y, upper-y); we can define the specific portion of the sheet we want to render without worrying about the dimensions being powers of 2.

To keep the code snippets in this tutorial concise lines pertaining to audio or text are not included but they do not need to be removed.

    constructor() {
        super();
        this.mCamera = null;
        this.mGameObj = null;
        this.mRenderable = null;
        
        this.kTexture = "assets/minion_spritesheet.png"
    }

    load() {
        engine.texture.load(this.kTexture);
    }

    unload() {
        engine.texture.unload(this.kTexture);
    }

    init() {
        
        this.mCamera = new engine.Camera(
            vec2.fromValues(50, 50), // position of the camera
            100,                        // width of camera
            [0, 0, 600, 600],         // viewport (orgX, orgY, width, height)
            2
        );
        this.mCamera.setBackgroundColor([0.8,0.8,0.8,1]);

        this.mRenderable = new SpriteRenderable(this.kTexture);
        this.mRenderable.setElementPixelPositions(130,310,0,180);

        this.mGameObj = new GameObject(this.mRenderable);
        this.mGameObj.getXform().setSize(20,20);
        this.mGameObj.getXform().setPosition(70,70);

        engine.defaultResources.setGlobalAmbientIntensity(4);
    }
Code Snippet 3-1: Adding a SpriteRenderable

Note: bold lines indicate changes introduced by this tutorial.

If you make the above changes to your code, you will notice that the scene looks exactly the same (besides the text). So far, this doesn't seem very useful until we consider that we can consolidate all of our textures onto a single sprite sheet and that our renderable textures can be of arbitrary dimensions.

Unlike a TextureRenderable, SpriteRenderable allows us to change the renderable being shown by changing the element pixel position of the sprite sheet. There is an identical minion facing the opposite direction at (720,0). The size is still 180x180 so calling this.mRenderable.setElementPixelPositions(720, 900, 0, 180); will allow our sprite to appear to change direction.

Our draw() function is going to be identical to our previous samples.

Below, we modify our update() function to make the sprite change directions based on the keyboard controls.

    update() {
        if(engine.input.isKeyPressed(engine.input.keys.A)){
            this.mGameObj.getXform().incXPosBy(-0.5);
            this.mRenderable.setElementPixelPositions(130,310,0,180)
        }
        if(engine.input.isKeyPressed(engine.input.keys.D)){
            this.mGameObj.getXform().incXPosBy(0.5);
            this.mRenderable.setElementPixelPositions(720,900,0,180)
        }
        if(engine.input.isKeyClicked(engine.input.keys.Q)){
            this.next();
    }
	
Code Snippet 3-2: Updating sprite

You can see the scene here.


Sprite Animation

We can take our SpriteRenderable concept another step forward by defining how to implement automatic animation. First, we create a variables for a second renderable and game object.

In our next example, we are going to setup two renderables. Our original renderable will still respond to keyboard input and our second renderable will follow the mouse location. The second renderable will also animate with a sprite sequence. You can see the results here.

Figure 3-3: Two sprites

First, we declare constants and load our resources.

    constructor() {
        super();
	this.mCamera = null;
	this.mRenderable = null;
	this.mGameObject = null;
	this.mAnimatedObj = null;
        this.mAnimatedRenderable = null;
    
	this.kTexture = "assets/minion_spritesheet.png"
    }
Code Snippet 3-4: Adding a second GameObject

During initialization, we will use a new renderable object type, SpriteAnimateRenderable. This provides the function setSpriteSequence(348, 0, 204, 164, 5, 0) to define an animation sequence. Each of the parameters are defined as follows...

After creating renderable, we define what direction the engine should cycle through the images with setAnimationType(eAnimationType.eRight) and how fast the cycle should move with setAnimationSpeed(12). These calls tell the SpriteAnimateRenderable to change sub-image every 12 cycles from left to right, then restart. Since the engine runs at 60 updates per second, 5 waits of 12 cycles means that the animation takes one second to complete.

    init() {
        this.mCamera = new engine.Camera(
            vec2.fromValues(50, 50), // position of the camera
            100,                        // width of camera
            [0, 0, 600, 600],         // viewport (orgX, orgY, width, height)
            2
        );
        this.mCamera.setBackgroundColor([0.8,0.8,0.8,1]);

        this.mRenderable = new SpriteRenderable(this.kTexture);
        this.mRenderable.setElementPixelPositions(130,310,0,180);
        
        this.mAnimatedRenderable = new SpriteAnimateRenderable(this.kTexture);
        this.mAnimatedRenderable.setSpriteSequence(348,0,204,164,5,0);
        this.mAnimatedRenderable.setAnimationType(eAnimationType.eRight);
        this.mAnimatedRenderable.setAnimationSpeed(12);
        
        this.mGameObj = new GameObject(this.mRenderable);
        this.mGameObj.getXform().setSize(20,20);
        this.mGameObj.getXform().setPosition(70,70);
        
        this.mAnimatedObj = new GameObject(this.mAnimatedRenderable);
        this.mAnimatedObj.getXform().setSize(16,12)
        this.mAnimatedObj.getXform().setPosition(40,40);
        
        engine.defaultResources.setGlobalAmbientIntensity(4);
    }


Code Snippet 3-5: Initialize with Animated Renderables

In the update function, we are going to control the motion of the second renderable using the mouse location. The engine's input routines can be used to get the mouse location but the coordinates will be in pixel space. To convert to WC, we need to take information from the camera object and perform math. Fortunately, this is very common so the camera object provides mouseWCX() and mouseWCY() to get the location in the proper coordinate system.

We also need to tell the renderable to update the animation with a call to updateAnimation().

    update() {
        if(engine.input.isKeyPressed(engine.input.keys.A)){
            this.mGameObj.getXform().incXPosBy(-0.5);
            this.mRenderable.setElementPixelPositions(130,310,0,180)
        }
        if(engine.input.isKeyPressed(engine.input.keys.D)){
            this.mGameObj.getXform().incXPosBy(0.5);
            this.mRenderable.setElementPixelPositions(720,900,0,180)
        }
        if(engine.input.isKeyClicked(engine.input.keys.Q)){
            this.next();
        }
             
        this.mAnimatedObj.getXform().setXPos(this.mCamera.mouseWCX());
        this.mAnimatedObj.getXform().setYPos(this.mCamera.mouseWCY());
        
        this.mAnimatedObj.getRenderable().updateAnimation(); 
        this.mGameObj.update();
    }
Code Snippet 3-6: Updating Animation

In the draw routine, of course, we draw our second renderable.

    draw() {
        engine.clearCanvas([0.9, 0.9, 0.9, 1.0]);
        this.mCamera.setViewAndCameraMatrix();
        
        this.mGameObj.draw(this.mCamera);
        this.mAnimatedObj.draw(this.mCamera);
    }
        
Code Snippet 3-7: Drawing Two Renderables

As long as the mouse is within the canvas area and the browser is in the foreground, the canvas will draw the second GameObject at the mouse location.


Collision Detection

The code we have created in this tutorial has set us up for looking at collision detection (after all, we need at least two objects to touch to see this in action). We call GameObject's pixelTouches() function to determine if the game object is in contact with another game object. The function returns a boolean and provides the WC coordinates of the point where the collision occurred.

    update() {
        if(engine.input.isKeyPressed(engine.input.keys.A)){
            this.mGameObj.getXform().incXPosBy(-0.5);
            this.mRenderable.setElementPixelPositions(130,310,0,180)
        }
        if(engine.input.isKeyPressed(engine.input.keys.D)){
            this.mGameObj.getXform().incXPosBy(0.5);
            this.mRenderable.setElementPixelPositions(720,900,0,180)
        }
        if(engine.input.isKeyClicked(engine.input.keys.Q)){
            this.next();
        }
        
        // we declare an array to store the point of intersection (not used by us)
        var h = [];
        if (this.mAnimatedObj.pixelTouches(this.mGameObj,h)) {
            this.mAnimatedObj.getXform().incRotationByDegree(2);
        }
        
        this.mAnimatedObj.getXform().setXPos(this.mCamera.mouseWCX());
        this.mAnimatedObj.getXform().setYPos(this.mCamera.mouseWCY());
        
        this.mAnimatedObj.getRenderable().updateAnimation();
        this.mGameObj.update();
    } 
Code Snippet 3-8: Update with Collision Detection

As we can see here, we check to see if there is overlap between the two GameObjects and cause rotation of the animated renderable when overlapping. Notice that our renderable continues to animate while it is rotating.


Conclusion

With the sprite sheet and SpriteAnimateRenderable, there is a great deal of potential in customizing the look of our game elements. Collision detection gives us a tool for working with behavior.

In Tutorial 4, we will take a further look at behavior using rigid bodies to resolve GameObject overlap after collisions. This will simulate GameObjects acting as solid objects when interacting with each other. We will also look at creating particle affects.


Tutorial 2 <-- Tutorial 3 --> Tutorial 4
Tutorial Homepage

5/19/2022 - By Myles Dalton