Sprites and Animations Using the 2D API

In our last article, we took an introductory look at using the BrightScript 2D API.  The coverage there was pretty high level, and focused on basics like drawing simple shapes, images, and text on single buffered roScreens.  In this article, we will explore some of the more advanced 2D API concepts and show how to build richer applications with the API.  We will look at things like compositors, sprites, and animations.

For this article, I’ve included two simple sample channels demonstrating the techniques described in this article.  The first sample is a sprite animation of snowflakes falling in a chilly winter scene.  Here is a screenshot from the channel:

The sample channel can be downloaded from this link.

The second sample, to be discussed later, is much simpler. It shows how to animate a single sprite, giving you a reference for the most fundamental aspect of developing animations. This second sample can be downloaded from this link.

Double Buffering

In our last article we focused exclusively on single buffered drawing in the roScreen.  With this technique, you make 2D API calls and then make them render on screen with a call to the ifDraw2D interface Finish() API.

Double buffering is the technique of rendering screen content such as video or animations using two buffers of memory.  Data in one buffer is rendered on the screen while the other buffer is preparing or processing the next frame of data to be displayed.  In effect, one frame of content is displayed while the next one is being prepared in the background.  The buffers are swapped when the background frame is ready.  The result is faster rendering with no image tearing.

The BrightScript 2D API implements double buffering through the roScreen SwapBuffers() function.  This function is used in place of the Finish() function that forces draw operations to complete in single buffered applications.

    screen = CreateObject("roScreen", true)
    port = CreateObject("roMessagePort")
    screen.SetMessagePort(port)
    ‘Perform various drawing, graphics, etc. operations
    screen.SwapBuffers()

To use double buffering, the roScreen instances that you create need to specify true in the second argument to the CreateObject() call.  The default, where no true value is passed, is to create a screen that only does single buffering.  All of the drawing that you want to do in a given frame using the 2D API is done the same as in the single buffered case.  Basic shape drawing, image rendering, animations, etc. are done as if the screen were single buffered.  The difference is that you call SwapBuffers() to render the frame.  All of the drawing operation calls that you’ve made get completed and the background buffer is swapped to the front and thus made visible to the user.  As a BrightScript developer, you don’t need to keep track of the buffers involved in the rendering.  All you need to remember is to create an roScreen for double buffering, and use SwapBuffers() to display your graphics.

Compositors and Sprites

The BrightScript 2D API provides great support for sprites and animations.  It is very easy to create channels and games that incorporate these elements resulting in rich graphical experiences.  Sprites are created and drawn using a BrightScript component called a compositor.  Compositors enable the composition and animation of multiple sprites or regions.  Compositors are implemented with the roCompositor component.  The ifCompositor interface which roCompositor implements contains drawing methods for rendering sprite content that the compositor contains.  A compositor is linked to an roScreen by calling the roCompositor SetDrawTo() function:

Void SetDrawTo(Object destBitmap, Integer rgbaBackground)

destBitmap is the destination to which compositor draw functions render.  The destination is normally the application’s roScreen instance.  rgbaBackground specifies the background color of the destination.  Often applications will load a separate full screen sprite to use as the background.  If you do this, you can set the SetDrawTo() background argument to 0.

Sprites are created using the compositor NewSprite() function:

Object NewSprite(Integer x, Integer y, Object region, Integer z)

The x and y arguments specify the location where the sprite will be placed on the screen when rendered.  region is the roRegion (portion of an roBitmap) to be drawn.  z is an optional argument that specifies the Z-order.

Here is an example of how to create a compositor, associate it with an roScreen, and then create a sprite on the compositor:

    screen = CreateObject("roScreen", true) ‘Create a double buffered roScreen
    compositor = CreateObject(“roCompositor”)
    compositor.SetDrawTo(screen, 0)  ‘Tie the compositor to the screen
    bmp = CreateObject(“roBitmap”, “pkg:/images/myimage.png”)  ‘Load the image
    region = CreateObject("roRegion", bmp, 0, 0, 128, 128)      ‘Create a region
    sprite = compositor.NewSprite(0, 0, region)  ‘Create the sprite

To draw the sprite on the screen, the ifCompositor interface defines two functions:

    Void Draw()
    Void DrawAll()

The first of the function, Draw(), draws any sprites that have changed in some way since the last call to Draw() or DrawAll().  For example, a change to the sprite’s x or y location would mean that the sprite has changed and is subject to being drawn.  Or the bitmap region assigned to the sprite could be changed.  In cases where you want to draw everything regardless of changes, use DrawAll().  This function forces a redraw of every sprite contained by the compositor.

Sprite Details

So far we’ve examined the basics of sprites at a high level.  Now let’s take a closer look at some of the key functions and features of sprites.  The roSprite component implements the ifSprite interface.  The location of a sprite can be changed using the MoveTo() function:

Void MoveTo(Integer x, Integer y)

This function sets the position of the sprite to the x, y location specified.  If you need to find the x or y location or z-order of a sprite, you can call:

Integer GetX()
Integer GetY()
Integer GetZ()

Sprites can also have application specific data assigned to them.  For example, in an animation, you might want to have different sprites move at different rates.  This could be implemented as different x and y location deltas that you store with the sprite.  This data can then be read as needed when moving each sprite.  For such purposes, ifSprite includes the SetData() and GetData() functions:

Void SetData(TVAL data)
TVAL GetData()

SetData() takes any typed value, including intrinsic types and objects, as an argument.  In the scenario we described of different animation rates, you could assign that data to a sprite as an associative array like this:

    dx = Amount to move X
    dy = Amount to move Y
    sprite.SetData( {deltaX: dx, deltaY: dy} )

Removing a sprite from a compositor is a simple matter of calling Remove():

    void Remove()

Calling this function removes the sprite from its compositor and then deletes the sprite from memory.

Sprite Animations

Sprites are often used to animate graphics in applications.  This is particularly common for games, screen savers and the like.  Animating sprites includes moving the sprites location around the screen, a topic we’ve just covered.  But animations can also include animating the sprites themselves, i.e., changing their orientation, rotating them, etc. whether or not they move around the screen.

To understand sprite animation, it is important to know about the concept of bitmap sets.  A bitmap set is a collection of regions, animation frames, and other information about resources used in 2D API applications.  A bitmap set is defined by an XML file packaged with the channel.  A typical bitmap set XML file looks like this:

<BitmapSet>

    <Bitmap name="sprite_bitmapset" filespec="pkg:/assets/bitmapset.png">
        <Region name="r1" x="0" y="0" w="300" h="300" />
        <Region name="r2" x="300" y="0" w="300" h="300" />
        <Region name="r3" x="600" y="0" w="300" h="300" />
        <Region name="r4" x="900" y="0" w="300" h="300" />
    </Bitmap>

    <Animation name="animated_sprite">
        <frame use="sprite_bitmapset.r1" />
        <frame use="sprite_bitmapset.r2" />
        <frame use="sprite_bitmapset.r3" />
        <frame use="sprite_bitmapset.r4" />
    </Animation>

</BitmapSet>

A bitmap set is loaded into a BrightScript channel using the function dfNewBitmapSet() defined in a helper library called bslDefender.  bslDefender contains a set of functions that simplify some 2D API tasks.  Details of this library can be found here.

Object dfNewBitmapSet(String filename)

This function takes the path to the bitmap set XML file as an argument and returns an associative array containing the bitmap set details.

A bitmap set can contain one or more <Bitmap> elements.  Each of these defines a single bitmap that can be further broken into a set of regions.  This is convenient since, as you’ll recall, sprites are created from bitmap regions.  In the bitmap set XML example above, pkg:/assets/bitmapset.png refers to the 1200 x 300 image shown below:

This image contains four views of a text string each in a different orientation. The section in the bitmap set XML file that refers to this image defines four regions, one for each 300 x 300 section of the source image at each of the four orientations.
The next section of the XML file shows how an animation is defined using these regions. An <Animation> element contains one or more <frame> elements. Each frame refers to a bitmap region. When the animation is played, each of the frames is rendered in order to provide the animation effect.
BrightScript channels can create animated sprites using the ifCompositor function NewAnimatedSprite():

    Object NewAnimatedSprite(Integer x, Integer y, Object regionArray, Integer z)

This function is similar to NewSprite(), except that it takes a region array argument, instead of a single region, which is used to define the animation frames.
Once an animation loaded, it is animated by calling the ifCompositor function AnimationTick():

    Void AnimationTick(Integer duration)

Each call to this function tells the compositor to move each animated sprite it contains to the next frame in the sprite’s frameset. The duration argument is used to specify the number of milliseconds that have elapsed since the last call to the function. The platform uses this value to smooth out animations depending on how much time elapses between animation ticks. It is thus possible to write frame rate independent animations.

To write frame rate independent BrightScript animations, the roTimespan component can be used to track the amount of time that elapses between AnimationTick calls.  roTimespan is BrightScript’s timer component.  A complete example of how to create and animate a sprite in this way is shown below.  This code is in the second downloadable example.

Library "v30/bslDefender.brs"
Function Main() as void
screen = CreateObject("roScreen", true)
port = CreateObject("roMessagePort")
bitmapset = dfNewBitmapSet(ReadAsciiFile("pkg:/assets/bitmapset.xml"))
compositor = CreateObject("roCompositor")
compositor.SetDrawTo(screen, &h000000FF)
clock = CreateObject("roTimespan")
clock.Mark()
screen.SetMessagePort(port)
screen.SetAlphaEnable(true)
sprite = compositor.NewAnimatedSprite(450, 225, bitmapset.animations.animated_sprite)
    while true
        event = port.GetMessage()
        if (event = invalid)
                ticks = clock.TotalMilliseconds()
            if (ticks > 1000)
                compositor.AnimationTick(ticks)
                compositor.DrawAll()
                screen.SwapBuffers()
                clock.Mark()
            endif
        endif
    end while
End Function

With each pass through the event loop, we make a call to roTimespan TotalMilliseconds().  This function returns the total number of milliseconds that have elapsed since the last roTimespan Mark() function call.  When the number of milliseconds is greater than 1000, we animate the sprite one frame.

The snow scene sample channel provided with this article uses the same techniques as the code above.  The main difference is that instead of creating one animated sprite, it creates numerous sprites.

You should now have a good foundation to start building your own animated 2D BrightScript channels.  Download and explore the samples, and as always, happy coding!
Click here for the snowflakes sample.
Click here for the simple single sprite animation sample.

About Robert Burdick

Roku Developer Support Manager
This entry was posted in Uncategorized. Bookmark the permalink.