Click here to Skip to main content
15,867,568 members
Articles / Web Development / HTML5

HTML5 Canvas : Clean JavaScript & Code Organization Allows Faster Dev, Easier Extensibility

Rate me:
Please Sign up or sign in to vote.
5.00/5 (9 votes)
3 May 2016CPOL10 min read 13.4K   173   13  
Less than 300 lines of JavaScript creates an interesting "Game of Life" example (random moving graphic creatures with lifespans - see animated gif).

Introduction

While writing another series of articles (starting here:   Algorithm : Calc Convex Hull & Draw HTML5 Canvas (Part 1 of 2)[^]) I began thinking about a simple game/novelty program which would dynamically display "living creatures".  I was also reading an old book on OOP and considering how each of those creatures should manage itself and what that code might look like.

The final program, which I call RobotDots is the result of that work.

animated robots

Live Example

You can see the final example right now at my web site:  http://raddev.us/RobotDots[^]

Background

Of course, while I created my little RobotDots the original  Conway's Game of Life - Wikipedia, the free encyclopedia[^] was in my mind though I knew nothing of its details.  Of course mine is far simpler and not near as interesting as the original.  Basically, the comparison ends at the idea of a group of "living creatures" are randomly generated and have some sort of lifespan.

In RobotDots each robot has:

  1. a birth (generated and appears on screen)
  2. an age (incremented as time progresses)
  3. lifespan (randomly generated value)
  4. a color (randomly chosen value from a set)
  5. opacity value (becomes more transparent as it ages)
  6. a size and maxSize (each object will grow to its individual maxSize)
  7. location (initially randomly generated x,y values then calculated movement)
  8. death (when the object moves off screen more than once or age is incremented to maxAge)

What Is The Point of This Code?

Self-Managing Objects

I wasn't attempting to create some grand game or anything that would entertain someone for a long time.  Instead I was interested in creating self-managing objects.  I wanted as much of the code to be encasulated in the main type which I call Robot.  

JavaScript OOP?

Next, I was interested in seeing if I could do this using JavaScript as if I were using a classic OOP language (C++, C#, Java) versus JavaScript (prototypal-based OOP).  

OOP Design In JavaScript? Is It Possible & Does It Help?

The real question I wanted to answer is : "Would I get benefit from following OOP Design even in JavaScript?"

Spoiler Alert

There is a huge benefit.  Your code becomes far easier to manage, extend, enhance, fix.  It is amazingly easy to think about complex solutions.

 

Quote:

If you can take the complex details and isolate them so you can work on them individually, you can make your problem far easier to work on.  That's the benefit of OOP.

 

Why JavaScript and HTML5?

My background is in C++ which then transformed into C# so there are languages I prefer over others.  However, I try to use the right tool for the job.  In this case JavaScript, HTML5 Canvas allow me to do the graphical work I want to do quite easy and of course it is available to anyone with a modern browser so it's easy to deploy.

How Do You Control the Action

  1. If you click anywhere on the board the game will pause.
  2. If you hold the Ctrl key and click a robot, it will become a master robot and draw lines to all of similar color. (you can also click the checkbox for this functionality -- on phones and pads)
  3. Clicking a robot will create a highlight ring around it so you can track the robot.
  4. Later I'll add points and other effects.

with master robot

Now, let's examine the code.  

App Start : OnLoad

Note: For more details on the libraries used (jQuery, Bootstrap) and the drawing of the background grid, you can read the first article in my other series here at CP (Algorithm : Calc Convex Hull & Draw HTML5 Canvas (Part 1 of 2)[^] ).

When the Browser's onLoad event fires, it will call our initApp() method, which runs just the one time when the app starts.  This allows me to intiailize some things we'll need throughout the app lifetime.  Basically, it sets up the grid background and initializes the Canvas object we will need to do all of our drawing.

JavaScript
function initApp(){
    theCanvas = document.getElementById("gamescreen");
    ctx = theCanvas.getContext("2d");
    
    ctx.canvas.height  = 650;
    ctx.canvas.width = ctx.canvas.height;
    

    theCanvas.addEventListener("mousedown", mouseDownHandler);
    intervalID = window.setInterval(mainGameLoop, 125);
    lineInterval = Math.floor(ctx.canvas.width / LINES);
    drawGameBoard();
}

SetInterval : Setting Up the Game Loop

The initApp() also sets up a timer that fires every 125 milliseconds. We set that up by calling the JavaScript method setInterval(). It's very easy to use, since you simply supply the method name (a reference to the method -- notice there are no parentheses) and an interval in milliseconds.  You can see that I've named the method which is called each time mainGameLoop().   That will serve as the main controlling loop of the entire app.

MainGameLoop

The main game loop is very simple since basically all it does is loop through the list of robot objects that are in our app-global array named allRobots[].  Most of the code that is executed is hidden inside each robot so it is easier to manage.

JavaScript
function mainGameLoop(){
    for (var idx = allRobots.length-1; idx >= 0;idx--){
        if (!allRobots[idx].isAlive){
            console.log("died at: " + allRobots[idx].age);
            if (allRobots[idx].isSelected){
                selectedCount--;
            }
            allRobots.splice(idx,1);
            if (masterRobot != null){
                if (!masterRobot.isAlive){
                    masterRobot = null;
                }
            }
        } 
        allRobots[idx].advanceAge();
        allRobots[idx].calculatePosition();
    }
    if (allRobots.length < 48){
      allRobots.push(new robot({x:genRandomNumber(600),y:genRandomNumber(600),color:getRandomColor()}));
    }
    drawGameBoard();
    drawRobots();
    if (masterRobot != null){
        drawConnectedRobots();
    }
}

Entire Summary of Everything the Game Does Is In Game Loop

Every 125 milliseconds this code fires and does the following:

  1. Iterates through the entire array of robots and removes any that have died (isAlive == false).
  2. Determines if there is a MasterRobot (more on this later) and if there is but it has died then remove it.
  3. advances the age of all robots (each interval of the game loop is time and robots age)
  4. calculates the new position of each roobt (represents robot movement on the screen)
  5. if there are less then 48 robots in the allRobots[] array then we add a new robot (one is born)
  6. draw the game board -- to represent the animation we have to (next step) draw robots in new locations, that means we need to redraw the game board each time too ( to erase it and clean it).
  7. check if master robot exists, if it does then drawConnecting lines to each of it's robots.

This is the power of OOP.  Complex work is hidden in each object (Robot object in this case) so we can easily summarize what the entire program does and only look at details if you want to.

Robot Class: The Power of This App

The robot class is where the details are hidden, however it's very easy to create a new robot.

If you wanted to create one all you have to do is the following:

JavaScript
var robot1 = new robot({});

Odd Syntax

If you haven't done much JavaScript programming you may think that looks quite odd.  Actually, even if you have, it may look odd.

That simple line of code constructs a new robot object by sending in an empty object, represented by { }.

Initializing objects in JavaScript can be done quite easily using this syntax, if you set up your object properly.

Let's take a look at a less complex sample so I can explain the concept more clearly.

Creating a Template Class

In JavaScript you create a template class by creating a function.  Here's an example:

JavaScript
function animal(initObj){
    this.name = initObj.name;
}

Now if you'd like to new one of those up, you can write the following code:

JavaScript
var cat = new animal({name:"cat"});

If you'd like to print the name of the animal in the console you can now do the following:

JavaScript
console.log(cat.name);

However, if the developer-user attempts to create an animal without sending in an object then he will get an error stating : Cannot read property 'name' of undefined.

That's because he sent in the null object.

We can easily fix that by sending in an empty (non-null object) like the following:

JavaScript
var cat = new animal({});

However, we may want a default value so we can use some more odd JavaScript to set default values when there is just an empty incoming object.  Let's alter the class so it looks like the following:

JavaScript
function animal(initObj){ this.name = initObj.name || "mammal"; }

Explanation of Odd Syntax

That syntax is a bit odd but it leverages the fact that when the incoming object is empty then the name return the undefined value (which is an object in JavaScript).  When you || that together with the valid value the valid value on the right side will be true and your name property will get set to that value.

Now, the default value for the name property will be "mammal".

Let's take a look at our robot object now and you'll see this type of initialization code at the top of its definition.

JavaScript
function robot (r){
    this.x = r.x || 200;
    this.y = r.y || 200;
    this.color = r.color || "black";
    this.size = r.size || 10;
    this.maxSize = r.maxSize || null;
    this.maxAge = r.maxAge || null;
    this.isSelected = r.isSelected || false;
    this.age = r.age || 1;
    this.isAlive = true;
    this.globalAlpha = r.globalAlpha || 1;
    this.offGridCount = 0;
    
    this.calculatePosition = function(){
        var flag = genRandomNumber(2);
        var addFlag = genRandomNumber(2);
        //console.log(flag);
        if (flag > 1){
            if (addFlag > 1){
                this.x += genRandomNumber(4) + genRandomNumber(3);
                }else{
                this.x -= genRandomNumber(4) + genRandomNumber(3);
                }
            }
        else{
            if (addFlag > 1){
            this.y += genRandomNumber(4) + genRandomNumber(3);
            }
            else{
                this.y -= genRandomNumber(4) + genRandomNumber(3);
            }
        }
        if (this.x >= 650 || this.x <= 0 || this.y >= 650 || this.y <=0)
        {
            this.offGridCount +=1;
        }
        if (this.offGridCount >=2){
            this.isAlive = false;
        }
        //console.log ("x : " + this.x + " y : " + this.y);
    }
    this.advanceAge = function() {
        // console.log("advanceAge...");
        if (this.age >= this.maxAge){
            this.isAlive = false;
            return;
        } 
        this.age +=1;
        // console.log("this.age : " + this.age);
        this.grow();
    }
    this.grow = function() {
        if (this.age % 100 == 0){
            if (this.maxSize == null || this.size < this.maxSize){
                this.size +=1;
            }
        }
        if (this.age % 200 == 0){
            this.globalAlpha -= .1;
            if (this.globalAlpha <= .2){
                this.isAlive = false;
            }
        }
    }
    this.drawRobotHighlight = function(){
        ctx.beginPath();
        ctx.lineWidth = 2;
        ctx.arc(this.x, this.y,this.size + 7,0,2*Math.PI);
        ctx.strokeStyle = "black";
        ctx.globalAlpha = 1;
        ctx.stroke();
    }
    this.drawRobot = function (){
        //console.log("robot size : " + allRobots[idx].size);
        ctx.fillStyle = this.color;
        ctx.strokeStyle= this.color;
        if (this.isSelected) {
            this.drawRobotHighlight();
        }
        ctx.globalAlpha = this.globalAlpha;
        ctx.beginPath();
        ctx.arc(this.x, this.y,this.size,0,2*Math.PI);
        ctx.stroke();
        ctx.fill();
        // reset opacity
        ctx.globalAlpha = 1;
    }
    this.initRobot = function(){
        this.maxSize = this.calcMaxSize();
        //console.log("maxSize : " + this.maxSize);
        this.calcMaxAge();
    }
    this.calcMaxSize = function(){
        return genRandomNumber(15) + 10;
    }
    this.calcMaxAge = function (){
        this.maxAge = genRandomNumber (40000) + 10000;
        //console.log ("maxAge : " + this.maxAge);
    }
    
    this.initRobot();
}

That feels like a lot of code, but let's just look at the methods that robot implements.

What Robot Does : Public Functions

Here's a list of everything a robot can do:

  1. calculatePosition() - generate values for X, Y positions to simulate movement
  2. advanceAge() - add to the age value to make the robot grow older
  3. grow() - add to the size value to make the robot grow in size
  4. drawRobotHighlight() - draw the outer circle which highlights the robot
  5. drawRobot() - draw the robot on screen to display itself
  6. initRobot() - calculate maxSize and maxAge which is used later to determine size and when it dies
  7. calcMaxSize() - called by initRobot to initialize the maxSize to a random value
  8. calcMaxAge() - called by initRobot to initialize maxAge to a random value.

That's all the code does.  Each time the mainGameLoop() fires all of the details behind those methods are hidden nicely so you can understand what is happening in isolation from everything else.

Examine an Example Method In Isolation

Okay, so even though there is what seems to be a lot of code, we can now look at one method and determine what it does.  Let's take a look at the drawRobotHighlight() method and we'll see how simple it is.  

drawRobotHighlight()

This is the code that draws an outer black ring around each robot if it is selected.  This could be very difficult because robots can all be different sizes.  However, since this code is encapsulated to run on each individual robot we have made it very simple.

JavaScript
this.drawRobotHighlight = function(){
        ctx.beginPath();
        ctx.lineWidth = 2;
        ctx.arc(this.x, this.y,this.size + 7,0,2*Math.PI);
        ctx.strokeStyle = "black";
        ctx.globalAlpha = 1;
        ctx.stroke();
    }

The line that does the actual drawing is the one that calls the ctx.arc() method.

You can see highlighted robots that have the circle around them in the following image:

highlighted items

We draw the current robot object at its x, y location (first two parameters to arc() method.  But we also need to draw it so it is always 7 pixels bigger than it's own radius.  This is very easy, because as you can see the third parameter is where you provide the radius size. In that case we simply add 7 to whatever the robot's current radius size is (stored in its size property).  Now, each time the robot grows and is drawn the highlight circle is always the right size in relation to the specific robot.

I'll let you examine the rest of the methods because they are self-explanatory (along with my other article I already referenced here).

However, I'd like to talk about the most interesting feature of creating a master robot. 

Creating A Master Robot

At first I hadn't thought about this idea of allowing the user to click any robot and have it draw connecting lines to every other robot of similar color.  It struck me later after I had written all the other code. I thought, "hey, if I have a collection of all the robots and I know all their center points and I can easily draw a line from one to the other."  

The Power of Focus

I was able think about this and focus on this functionality in isolation from drawing each robot in its location on the screen.  Now it simply became an exercise in iterating through the points and drawing lines to ones which have the same color value.  Here's the code. It'll all make sense and the effect is (I think) very cool.

JavaScript
function drawConnectedRobots(){
    for (var i = 0; i < allRobots.length;i++){
        masterRobot.isSelected = true;
        if (masterRobot.color == allRobots[i].color){
            drawLine(masterRobot, allRobots[i], masterRobot.color); 
        }
    }
}

It's all quite simple in isolation from all that code which draws the robots in their locations.

I hope this article has made you think about one possible additional use of OOP that you might use to simplify difficult code.

History

First release : 05-03-2016

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) RADDev Publishing
United States United States
"Everything should be made as simple as possible, but not simpler."

Comments and Discussions

 
-- There are no messages in this forum --