Expect this Unit to last between 3 to 5 hours. To demonstrate what you should have at the end of this Unit, check out the Demo for Unit 8.
There will be a pretty rewarding Unit as you'll finally starting seeing stuff on the screen, that with very little effort can become interactive! There's a whole lot of content that we could cover, but I'm going to focus on code structure and framework, so we can quickly move onto the even more exciting unit on Sprites.
Rendering is typically a pretty heavy subject matter in games development. Rendering pipelines can be quite large, the technology often bleeding edge. Great visuals are such an important part of games so it is critical that we build our engine to support. Lucky for us this is actually one of the greatest strengths of HTML5 games, as rendering is something that's primarily handled by the browser.
In HTML5 games, we actually have a couple of choices for displaying things on the screen. We've already been using text and displaying Images directly in the DOM. One option is using SVG, which stands for Scalable Vector Graphics, and allows you to render things much like Flash. We could also use CSS transitions to make things animated and pretty. However, the best option for this class is Canvas, as using it most closely resembles development for native made games.
The HTML5 <canvas> element is used to draw graphics, on the fly, via scripting. Canvas has several methods for drawing paths, boxes, circles, characters, and adding images.
There is a wealth of resources on the internet that explain the history and evolution of the HTML5 canvas. This class isn't meant to make you an expert on what canvas is and on its many features, but my goal is to make you sure you're comfortable with using it as a drawing surface. We'll be drawing lines and paths and rendering images in order to display sprites. I won't go into the Canvas API in depth, but I highly recommend reading through this section of Dive Into HTML5 on Canvas called LET’S CALL IT A DRAW(ING SURFACE). It's midly lengthy, but does a good job at explaining how canvas (and pretty much any other HTML5 feature) works and preparing you for this Unit.
Canvas has a whole lot of features, but there's only two I'm going to be concerned with for this Unit:
- clearRect() - clears the pixels in the specified rectangle.
- drawImage() - draws an Image on the canvas
It's very likely that we would only need a single canvas to draw our sprites to. Or it's also possible that we would want more than canvas, and may not want all of them visible to the user. We might have an offscreen canvas that we draw into procedurally, and copy over to the on-screen canvas every frame. Or we could have a couple of on-screen canvas objects, either on top of each other or at different locations on the page, and will be drawing to each of them in different ways.
These kinds of choices can have an interesting impact on performance, but those decisions generally need to be made at game code time. For now we are going to do our best to support multiple canvas objects that can be configured to be on screen or not.
In this Unit we will focus on two things:
- Create a wrapper for the HTML5 Canvas object, which we'll call RenderContext2D.
- Extending the Engine with a small sub system for managing multiple RenderContext2D instances.
Our canvas wrapper is going to inherit from DrawNode. This is so that when we tell the canvas to draw, any object attached to it can draw with an appropriate render order given by the zOrder property.
Anytime we create a new instance of a RenderContext2D we need it to manage a separate instance of a canvas object. The base functionality we'll need this object to have is:
- init() - constructor function will create a new instance of canvas
- resizeCanvas() - if the user changes the browser window size, we might want to respond to it
- getContext() - accessor function to retrieve the render context
- drawCanvas() - this canvas's turn to draw
- clearCanvas() - clear the entire canvas
I've added a list of default properties that we will use in the implementation.
First, because this is a derived class type, we need to call our parent class constructor through this._super(). We had a good break from OOP in the last Unit didn't we!
This class is assuming that we want to create a new canvas object, although it could easily be changed so that we support accessing on from the DOM.
We want to get the 2D rendering context from the canvas, which is what we'll actually be using to draw things to the screen. Right now "2d" is our only option, but this flexibility to take in other options is required by the HTML5 specification, in anticipation of eventual 3D canvas support.
backgroundColor and zIndex are both CSS properties that this class uses to pass along using jQuery.
You'll probably remember that creating a new HTML object doesn't actually add it to the DOM. If we are given a container property then we know this is an on-screen canvas and it needs to be inserted into the DOM. The easiest way to get a container to pass in here is through jQuery: $('#some_div_id')
We are supporting two features with setting the canvas size: either it is full screen taking up as much of the window as possible, or it is set to a fixed size.
Usually we'll want to clear the entire render context and then draw all of the sprites associated with it. Just in case we don't want to clear first, check a flag before using the Visitor pattern built into the DrawNode object to call .draw on any child objects.
This is essentially a manager of canvas instances. The implementation will follow the same path the Asset Manager took by adding new functionality to the Engine itself. It will also follow the same behavior of storing these objects so they can be accessed under an alias.
There is another programming pattern we are using here, called the Factory pattern. The engine is now a factory for creating new instances of the RenderContext2D class. This means it also needs functions for destroying a canvas.
Along with having functions to clear and draw all created canvases, we also are going to have a function that we can register with the engine as a game loop function.
First let's take a look at the API, and then we'll dive into the implementation for each function.
- createCanvas() - factory function, creates and stores a canvas
- getCanvas() - Access a canvas by name
- destroyCanvas() - destroys a single canvas
- destroyAllCanvases() - destroys all existing canvases
- clearAllCanvases() - clears all existing canvas (clear screen)
- drawAllCanvases() - tells all canvases to draw
We are also going to have a helper function called canvasGameLoop() that will be explained below, along with local storage for canvas objects.
Here is the implementation for the RenderContext2D factory function.
The code is very straightforward, although the is one bit at the end that might require a deeper look. If there currently no game loop registered, go ahead and register this helper function that makes sure all canvas objects get drawn.
It returns the canvas (which is also a DrawNode) so that the user can attach child objects to it and have them get drawn as well.
Easy accessor function for a given canvas. It only needs to do a safety check to warn the user in case a canvas doesn't (or no longer) exists.
Destroying a RenderContext2D is as easy as calling .destroy(), however we need to make sure we remove all references to the object.
Call destroy on all canvas objects we've created, and clear out both of the lists we have.
A very simple wrapper function, to call .clearCanvas() on all created canvas objects.
Now here's something fun! If want an easy way to make sure all canvas objects we create get rendered, then we have this utility function to register as a game loop function with the engine.
Registering this function using the engine's setGameLoop() from Unit 3, will make sure that each frame .drawAllCanvases() gets called.
Time to pull a whole bunch of pieces together! We'll use GameObjects, the Asset Manager, and the Canvas system to draw a sprite to the screen!
We haven't gone over the API for how to actually draw an Image to a canvas (we will in the next Unit), but it's very straightforward. Once we have an object that can draw itself, we just need to attach it to our canvas and presto!
Part 1. Create and add the two files to your project: renderContext2D.js and backgrounds.js
- Create the files and put them all in your /js_engine sub-directory.
- Add the files to the list of engine files to load through modernizr in main.js
Part 2. Fill out both files with the code from this Unit.
Part 3. Put it to work in your test code in main.js:
- Create a canvas that gets added to the DOM (you may need to update your html - create a <div> and give it an 'id' to use)
- Create a new GameObject that will behave as a sprite
- Move the position of the object every frame. Try using Input events to trigger changes in the sprite's position.
This is where we really start getting room to expand on features and functionality, so I'll list off a few ideas on how you might take this section further. Reading the Canvas API should give you some ideas.
- Do you know how you would be able to access an existing canvas from the DOM instead of always creating one? Hint
- Path drawing is a great way to render game objects without having any art assets. It could also be very handy for debug drawing! The API is straightforward, so how you wrap the functionality would depend on how you plan on using it. API Hints
- Another thing canvas supports is text drawing. It's probably best for performance to stick to using text in the DOM, but if you wanted in game text to be drawn in canvas (without using sprites to do it), you could look at the text drawing API. API Hints
- Canvas can create another object called a Pattern, which could be useful for things like repeating backgrounds. How would we encapsulate that funcitonality into the RenderContext2D class? API Hints
- Draw a sprite that is somewhat transparent. API Hints
- Draw an image or something onto an off-screen canvas, and copy that entire canvas over to an on-screen one. API Hints