How Itopia works on PlayMycode

I'd originally intended to make Itopia into a game on PlayMyCode, but unfortunately, HTML5 performance isn't quite good enough (and is seriously rubbish when using Firefox!) to warrant any further development.



Instead I've moved it over to a desktop version (using libGDX) and will continue from there.

I did quickly try the same effect in the Swarm game I'm working on, and it looks smashing:



An image doesn't really do this justice - it looks absolutely lovely in motion :)

Unfortunately, the rest of the game doesn't quite fit now:



I'm now rebuilding the assets so they're more in keeping with the new look.

But what of Itopia on PlayMyCode?  Well, until performance improves (not a fault of PlayMyCode I might add) then, not much.  Instead, I've decided to break the code down here for anybody that's interested.

Have fun :)

Project Assets:


There's two main images used in this project:

    blob.png
    boomerspawn_spr.png

blob.png is used for almost everything in the demo! It's just shaped to do what we want.  Boomerspawn_spr is used to give the "head" of the red devil a more defined look. It's quite amazing what you can do with limited assets :) 

The music is called Factor and is by the talented Deceased Superior Technician.  You can find a lot more of his music here:

    NoSoapRadio

His music is free for any use (even commercial) - all you have to do is credit him appropriately :)

Code Structure:


The code is structured in the same way as I do all my projects on PMC.  There's the globals at the top, followed by the main game loop, defined by the onEachFrame() do |delta| . The remainder of the code comprises all of the classes used (described below).

There's a couple of points to make before delving into the classes...

1) There's an empty image asset called $mainCanvas that has the same dimensions as the screen.  Everything is drawn to this image rather than the main PMC canvas - hence why you'll see its definition being passed between classes/methods.  This is often termed as a "back-buffer". When everything has been drawn to this canvas, it is itself drawn to the main PMC canvas.  But why? Isn't this wasteful?  Well, yes it is a little and for the purposes of the Itopia demo, it could be removed.  However, I'd planned to use it to do other effects such as motion blur or other post-processing effects. It's up to you if you keep it like that or not :)

2) You'll notice the following in the globals section:
$blob       = new Image("blob.png")
$blobR      = tintImage($blob, :red)
$blobB      = tintImage($blob, :blue)
$blobG      = tintImage($blob, :green)


This code is pretty interesting and is something that Joe (one of the PMC developers) showed me.  It turns out that the setColor(...) and drawImage(...) functions, when combined, are very expensive in performance terms.  Instead of using setColor() it's better to pre-colour your images.

Above, we have the original blob image and then Red, Blue and Green versions - created using the following tintImage() function:

def tintImage( img, colour )
    temp = new Image( img.getWidth(), img.getHeight() )
    temp.setColor( colour )
    temp.drawImage( img, 0, 0 )
 
    return temp
end



Now, instead of using setColor() within the main code, we just use one of the pre-coloured images instead.


The main loop provides one parameter, delta in its block and is used to adjust the timings for entity movement. (see getDelta() in the reference documents).


The main loop clears the screen, ready for rendering and then also clears out the temporary canvas:


fill(0, 0, 0)
$mainCanvas.clear()
$mainCanvas.fill(20, 20, 40, 1.0)

$mainCanvas.setBlend(:add)


The setBlend(:add) is important here.  This defines that we want to use additive blending when drawing images.  Additive Blending is where the RGB colour elements of one image are added to the RGB elements of overlapping images.  If the two images are identical (in terms of colour) then when overlapping they will become brighter.  Itopia uses this to great effect, but the downside is the cost in performance.  In the demo, try creating a large number of jellyfish - you'll soon see the FPS plummet!  


if ($controls.isLeftPressed())
    $game.addJelly($controls.getMouseX(), $controls.getMouseY())
end
if ($controls.isRightPressed())
    $game.addRedDevil($controls.getMouseX(), $controls.getMouseY())
end


The above lines instruct the game class to add a new entity - a jelly if the left mouse button is pressed and a red devil if the right mouse button is pressed.  



$game.update(delta)
$game.render()


The above informs the game object that it must update and then render all of the entities it knows about.


setColor(:white)
drawImage($mainCanvas, 0, 0, false)


These last lines draw our temporary canvas to the screen.


Game class


The game class is the controller for all of the entities and related actions.  Itopia isn't really a game yet, so this is fairly simple.


You'll see that in the globals section (at the very top of this project) is the creation of an instance of this class:

$game = new Game()

This is then accessed from within the main loop, the key elements being the update/render calls.

def new()
    // Array that will contain all our entities (Jelly or RedDevil)
    @entities     = []
    @numEntities  = 0
     
    // Create a new instance of our background particles (max of 100 particles)
    @dust       = new Dust(100)
end



The constructor for the game class defines an empty array for all of the entities.  As the Jelly and RedDevil classes are actually sub-classes of Entity, we can manage them as Entity classes - no need for separate arrays for each type.

We also create an instance of the Dust particle generator.

def update(delta)
    @entities.eachIndex() do |i|
        @entities[i].update(delta)
    end
end



In any update operation I'll tend to use the eachIndex() operation rather than each(). This is a habit I've developed as an update in one of the classes could cause the array I'm iterating over to change (could even delete entries from it!).  eachIndex() handles this case so there's no nasty issues lurking here :)
 
def render()
    @dust.render($mainCanvas)
    @entities.each() do |e|
        e.render($mainCanvas)
    end
end


The above issues a render call to the dust particle generator first as we don't want these particles appearing in front of the jelly's.  One improvement could be in this area - use 3D perspective projection and have these particles in 3D along with the jelly's :)

Note, unlike the update() function, the render() function doesn't use eachIndex but uses each instead. This is because all updates have finished so no objects will be added/removed from the array.


The remainder of this class is just helper methods that are used to add or remove new entities, be them jellyfish or red devils.


Entity class


In game development, it's often the norm to create an Entity class that encompasses many of the common elements associated with (most, but not all) of your game "things".  

In Itopia, we have the Jelly and the Red Devil.  Both of these are entities and exhibit similar characteristics.  Now, it would be pretty silly to have the identical code in these classes for such things as location, velocity etc.  Instead, we have an Entity class that includes all of these common parameters.

The class itself should hopefully be fairly explanatory.


Jelly class


Ok, time for the interesting bits!


def new(x, y)
    // Pass the initial parameters to the parent Entity
    super(x, y, 1, 1)

    // The array of tentacles for this jelly
    @tentacles = []
     
    // Create three tentacles with random lengths, speeds
    @tentacles.push(new Tentacle(x, y, rand(0, 5), rand(15, 25)))
    @tentacles.push(new Tentacle(x, y, rand(10, 15), rand(15, 25)))
    @tentacles.push(new Tentacle(x, y, rand(20, 35), rand(15, 25)))

    // The movement of the jelly is defined by a simple math equation. Time is
    // a function of this equation and having a random value will position
    // the jelly at a different location to previous ones
    @time  = rand(0, 1000)
     
    // Speed of this jelly
    @speed = rand(0.1, 0.6)
end


The above is the constructor for the Jelly class.  As it's a sub-class of Entity, the first thing we do is pass the initial parameters to the constructor of the Entity.  These are simply the X/Y location.  The velocities (VX/VY) aren't used here so we pass through 1,1

Each jelly will have a number of tentacles attached to it.  The @tentacles array holds all of them.
In this demo, a jelly has three tentacles and they are created (using random length and initial time component) and pushed to the tentacle array.

As described in the code comment, the @time variable is a function of a simple math equation that governs the movement of the jelly.  Giving it a random number has the effect of positioning the jelly at a different location.

The @speed variable governs how quickly the jelly moves about the path defined by the math equation given in the update() function below.

def update(delta)
    // Increment the time by the speed
    @time = @time + @speed
     
    // Is it time to adjust the speed?
    if (@time.floor() / 1000 == 1)
        @speed = rand(0.1, 0.9)
    end
     
    // Calculate new X/Y positions
    setY( (15 *
         (@time * -6).toRadians().cos()) +
         (240 + (180 * (@time * 1.3).toRadians().sin()) ) )
    setX( (15 *
         (@time * -6).toRadians().sin()) +
         (320 + (200 * (@time / 1.5).toRadians().cos()) ) )
           
    // Update each tentacle
    @tentacles.eachIndex() do |t|
        @tentacles[t].update(X(), Y()+8, delta)
    end
end



This is the main update function for the Jelly.  The movement is controlled by the mathematical equation that is really just an wonky looking sine wave.  Try drawing the points generated by it and then drawing a line between them to see how it looks.  Also try adjusting some of these values to see what happens!

Once the new position has been calculated, each of the tentacles are updated - the head of each tentacle is always at the same position as the head of the jelly.

And finally, we draw the jelly

def render(canvas)
     
    // Draw all the tentacles
    @tentacles.eachIndex() do |t|
        @tentacles[t].render(canvas)
    end

    // Draw a blob for the head of the jelly
    canvas.setAlpha(0.3)
    canvas.drawImage($blobB, X(), Y(), 96, 64, true)
    canvas.drawImage($blobB, X(), Y(), 64, 56, true)
end



The tentacles are drawn first (by issuing a render() call on each tentacle).  Following this, a couple of (fairly dim) blue blobs are drawn to define the head of the jelly.  This should really look a lot better, but is ok for a first attempt!



Tentacle class


The tentacle class is probably the most complicated part of the project, but even so, is still very simple.

If you want a better understanding of the math used in both this and the Jelly class, try breaking each of the movement equations down and plotting them out.

Playing with the numbers within here can end up with some pretty wacky behaving tentacles!

I'll break the update function down here to help explain it better:

def update(headX, headY, delta)

     
    // Increment time by a fixed amount (not too fast)
    @t = @t + 0.005
     
    // Set the first node of the tentacle to be the position of the jelly head
    @nodes[0] = [headX, headY]



As with the jelly class, the movement is partially controlled by a function of time. This time is incremented based on the speed of tentacle movement (which in this case is fixed at 0.005).

The very first node of the tentacle must always be the same as the head of the jelly - otherwise it would look pretty strange, having tentacles appearing all over the place!

The position of all subsequent nodes of this tentacle are updated based on the position of the first node.  The following loop iterates over each node (except the first one as that was defined above).
     
    // Update subsequent nodes
    1.upTo(@numNodes) do |i|
        distX = @nodes[i-1][0] - @nodes[i][0]
        distY = @nodes[i-1][1] - @nodes[i][1]

//            dist  = ((distX * distX) + (distY * distY)).sqrt()

        // Simple method to calculate the distance between two points. Not very accurate
        // but a lot faster than the sqrt() method above
        if (distX*distY < 0)
            dist = distX-distY
        else
            dist = distX+distY
        end



Ok, the above looks a little strange.  What we're trying to do here is determine the distance between the current node and the previous one.  Knowing this will allow us to move it by a small amount in the correct direction.

The distance between two given points (x1, y1) and (x2, y2) can be calculated using a Pythagorean Theorem:

      d = square root of


    or in code terms:

        distance = sqrt( (x2-x1) * (x2-x1) + (y2 - y1) * (y2 - y1) )

    which can be made simpler by breaking it down as:

    xDistance = (x2-x1)
    yDistance = (y2-y1)

    distance = ( (distanceX * distanceX) + (distanceY * distanceY) ).sqrt()

    However, you'll see in the code above that I've commented out the line with the sqrt() operation!  As performance was an issue with this demo I tried all sorts of ways to improve it.  The sqrt() operation can be fairly slow so I opted for a simpler, although prone to error and less accurate, approach.

    Based on the distance we've just calculated, we now move the node of this tentacle by a small amount (30% of the distance calculated)

            // Adjust the position of this node by a small amount of the distance (30%)
            @nodes[i][0] = @nodes[i][0] + (distX * 0.3)
            @nodes[i][1] = @nodes[i][1] + (distY * 0.3)



    We could just leave the movement as above, but it doesn't really look like the movement of a tentacle - I wanted something a bit more believable, so the following two lines add the necessary "wiggle factor".

            // Now determine a "wiggle" factor that makes the tentacles more believable
            mX = (@t*2+15).cos() * (15 * (i-1)/(@numNodes-1) + 1*(1-(i/@numNodes)))
            mY = (@t*8).sin() * (15*@mSpeed * (i-1)/(@numNodes-1) + 3 * (1-(i/@numNodes)))



    If you imagine a sine wave and that each node of the tentacle exists at a point on this wave, then you can see that the tentacle would form what looks like a sine wave.  However, that's not so good looking on its own.  So, we adjust the phase and amplitude of the wave so that the movement is less so at the head of the tentacle and more so at the end. The above determines the value for the current node which is then applied:

            // And update the node positions again
            @nodes[i][0] = @nodes[i][0] + mX / i
            @nodes[i][1] = @nodes[i][1] + mY / i
        end



    So that's the movement out of the way, but there's one last thing here.  If you look at the demo again, you'll see two bright "lights" moving down the length of the tentacle. This is actually just a change in the brightness of certain nodes as they are drawn (see render() below).  Which node is chosen to be bright, and how long they should shine for is calculated by the following logic:
         
        if (@brightTimer.isExpired())
            @brightTimer = new Timer(50)
            @bright = @bright + 1
            if (@bright >= @numNodes)
                @bright = 0
            end
        end
        if (@brightTimer2.isExpired())
            @brightTimer2 = new Timer(70)
            @bright2 = @bright2 + 1
            if (@bright2 >= @numNodes)
                @bright2 = 0
            end
        end
    end

    The last part of the tentacle class is the render() function:

    @numNodes.times() do |n|

        s = 1 + (0.5 * (n * 25).toRadians().sin())

        canvas.setAlpha(0.05)

        canvas.drawImage($blobB, @nodes[n][0], @nodes[n][1], @sizeX*s, @sizeY*s, true)

        if (n == @bright || n == @bright2)

            sz = 5

            canvas.setAlpha(1.0)

            canvas.drawImage($blobW, @nodes[n][0], @nodes[n][1], sz, sz*s, true)
        else
            sz = 5
            canvas.setAlpha(0.4)
            canvas.drawImage($blobB, @nodes[n][0], @nodes[n][1], sz, sz, true)
        end
    end



    This should be fairly self explanatory.  For each node of the tentacle, we draw a fairly faint blue blob, the size of which is defined by "s = 1 + (0.5 * (n * 25).toRadians().sin()".  This creates the "bulges" in the tentacle.

    If the index of the current node being drawn is the same as the @bright or @bright2 variables, then we draw the blob image with an alpha of 1.0 (making it really stand out).  If it's neither of these then we just draw a slightly dimmer blob image for this node.


    RedDevil and RedTentacle classes


    I won't go over these here as they're very similar to the Jelly/Tentacle classes.  In fact, the RedTentacle class is a sub-class of Tentacle, but overrides the Render function to draw things a little differently.

    I'll leave these two for you to figure out :)



    Dust and DustParticle classes


    Ok, I admit - worst name for a class ever!  Dust - under the sea!  My excuse - if you can call it one, is that I was in a rush and couldn't think of a better name :)

    Having a plain black background proved a little boring so I added a small effect which randomly places tiny particles all over the screen.  Each of the particles will have a random alpha level and will move about, very slowly, until it's life is over.
    When a particle eventually dies, it resets itself - having new random parameters. Doing this, rather than creating a new particle, reduces the CPU overhead as we're not creating or destroying resources.

    The Dust class is merely a container for all of the particles.  It has one render(canvas) function that will iterate over all of the particles, updating them first and then drawing them.

    Well, that's about it.  Feel free to ask any questions and I'll do my best to answer.


    Popular Posts