?

Log in

No account? Create an account

t3knomanser's Fustian Deposits

MPong

How Random Babbling Becomes Corporate Policy

run the fuck away

Mad science gone horribly, horribly wrong(or right).

MPong

Previous Entry Share Next Entry
run the fuck away
I wrote a JavaScript/XHTML based Pong game- mostly a proof of concept. I'm going to use this as the base of a browser-based Mario Brothers clone (not SUPER Mario- I'm not going to make this damn thing scroll if I can help it). That's a long-run project though. For now, I've got some basic plumbing done. It's got some bugs, etc- but I figured that since I had a (mostly) working library, I'd post my work so far, and share out the code.

Take the game for a spin, and grab the code.

Masturbatory Pong - My Design


The design of my XHTML/JavaScript game can be broken down into several areas. What we'll do is examine each area in turn, discuss some of the whys-and-wherefores, and get a feel for how you can recycle that code later.

General Design Goals


The biggest design goal was reusability. To make reusable and flexible code, the biggest rule is weak-coupling. Nothing should have any dependencies on anything else. To meet that goal, I aggresively applied OOP principles and Aspect Oriented Programming. Some of this breaks down on the game itself, simply because that section contained application specific code, and I didn't care that much at that point. It's still weakly coupled, but not cleanly written.

Program Components

Core


The DOM has all sorts of annoyances- mostly created by browser incompatabilities. Every developer, in every language, starts to develop a library of utility functions that they use again and again. In my case, the utility/library functions are contained in a file entitled DOMhelp.js.

Originally, this file came from a book on JavaScript(Beginning JavaScript for Practical Web Development, Including AJAX). My incarnation bears little resemblence, save for event-management. It provided good utilities for handling events in a browser-independent fashion.

The DOMhelp file contains a DOMhelp object literal. This is where the DOM event management code goes, as well as my custom functions. The important ones, for this application, are the "getControlLocation" and "translateClientToElementCoords". These are used to translate coordinates from the page area on screen to the element that will contain the game.

Also in that file is the EventSource aspect. These sorts of functions are my approach to AOP in JavaScript. It takes a class as its parameter and appends a bunch of functions to that class prototype- in this case, it appends functions for managing and firing events. At some point, for performance, I may modify this to have the event-firing event code run in a seperate thread. As it is, it works fine so long as there aren't many listeners. It's a pretty standard implementation.

This is my growing library of supporting code that I use in nearly every application.

Animator


The animator components are split across three major files, each specific to one function. The animator API is built around Sprites that can do their own frame management and collision detection, etc. Everything else is built to support those functions.

Sprite.js

The Sprite - General

This was my starting point for designing this library. The Sprite class should do as little as possible- it should focus on rendering and providing the graphical aspects of the application. Any other functionality it requires should be appended via Aspects.

The constructor requires a great deal of information, and all the parameters are documented. The only one that's really critical is windowFrameId- this tells the Sprite where to render its details.

The Sprite - Frames

For visual behaviors, the Sprite class tracks frames- each frame is stored in memory as an IMG element (for pre-loading). The Sprite tracks the frames in a set of arrays- each array represents a "frame-set"- a group of related frames that should display in a sequence. If you think of Super Mario, one series of frames plays when he walks left or right, jumps, or shoots a fireball, etc. It's all one Sprite, but different animations play based on the current application state.

Frame-sets are defined when frames are added- the "addFrame" method takes a frame-set as a parameter. If the parameter isn't set, the Sprite puts frames into the default frame-set. In the case of the MPong application, we only use one frame-set for every Sprite. It would, however, be trivial to add more frames and frame sets to make a more graphically exciting game. I will probably updated it with better graphics and add logic to the collision detection system to switch between frame-sets to provide visual feedback.

The Sprite gets quite interesting in the rendering process. Before we can really look at the rendering process, it's important to discuss the three aspects also defined in this file and their role in the process.
The Sprite - Aspects

The core Sprite code assumes that the Sprite has a location, a size, etc. but isn't responsible for maintaining those values. These values are appended to the Sprite class via Aspects. The Changeable aspect is the base here- it adds a boolean to track if the Sprite is "dirty"- only render dirty Sprites (for performance). A sprite HasLocation, HasSize- these are the keys for rendering. These allow us to track the position and size of a Sprite- which can then be rendered using floating DIV elements.

It is worth noting the CollisionDetection Aspect, even though it has nothing to do with rendering. Based on the size and position of a Sprite, it has a collection of methods to check for collisions. Right now, it only supports rectangle-boundary collisions, but this could be easily expanded- without altering the Sprite class. This flexibility is why I chose to use Aspects.
The Sprite - Rendering

The "render" method is the entry point for the rendering process. It only does something if the Sprite has been marked as "dirty" (changed==true). If you glance through the code, you'll notice that changing frame-sets, advancing between frames, changing positions or sizes will all dirty a Sprite.

Sprite rendering actually focuses on the "getContainer" method- this one is responsible for the real work of interacting with the DOM. This is the lowest-level method in the Sprite. It operates as a Factory Method wrapping a Singleton design pattern. It only creates the "container" once- a DIV element appended to the "windowFrameId" element. The "renderFrame" method is strongly-coupled to the structure of this DIV and its children, but that's trivial- the "getContainer" method just needs to always ensure that the first child of the container is the IMG responsible for displaying the current frame.

Caveat: This does not set a z-order on the element. This is probably something that should be added (especially because z-order used in conjunction with a scaling logic could create "3D" UIs).

Once we've got the container, "render" calls out to its Aspects to handle the job of positioning and sizing the DIV- again, we want the Sprite to be responsible for looks and looks alone. We'll rely on appended Aspects for the positioning/sizing logic. Once that's done, "renderFrame" is invoked- it simply updates the SRC of the IMG holding the frame. I'm fairly certain this is more effecient than modifying the DOM tree, but I haven't benchmarked it out.

motion.js

Movement - General

I've said this a lot, but one more time- the Sprite should only be responsible for rendering itself. All other logic should reside elsewhere. For logic that the Sprite directly depends on- like size and position- it does reside in the class via Aspects. Movement is a complex area, and it should be even more weakly coupled to the Sprite.

For this reason, I used a Provider model. The general formula is that a Provider tracks a list of sprites. It contains some algorithm for repositioning those sprites. The skeleton of this code is implemented in the GenericMotionProvider- "move" being the main method for the algorithm. What is important to note is that the GMP does not contain any logic for actually moving. It invokes an abstract method, "provideMotion". This should be implemented in child classes.

There are two child classes in the API at this time- certainly something I intend to expand. One is a LinearScrollMotionProvider, responsible for any straight-line one-direction motion. The other is a MouseMotionProvider, which allows mouse-following motion. This last would be trivial to modify into a drag-motion provider.

The biggest disadvantage to the design as it stands now is that the timer-driven providers (LSMP) have to be registered with a Clock, but event-driven providers (MMP) don't. This means that the client-code has to know which is which- I should probably modify this so that timer-driven providers take a clock in their constructor and register themselves, and not have the client code responsible. Fortunately, there's nothing explicitly wrong with registering the MMP with the Clock- it would just cause one more positioning cycle than is required. With something like the MMP, which is tracking a large number of events, one more cycle probably won't hurt performance much.
Motion - LinearScrollMotionProvider

The LSMP focuses on three major numbers- vert, horiz and increment. Wrap explains what to do when you reach the boundaries of the cavas area- if wrap is true, wrap around to the other side. Otherwise, stop.

Vert, horiz and increment all work together to represent a motion vector. The math is simplified, but that's the essential purpose. Vert represents the extent of the of the vector in the vertical axis, horiz in the horizontal. Increment is the "scaling factor" on the vector. This does have one disadvantage- changing the vert or horiz values can change the speed. In MPong, that's fine- actually desired. It may be wiser, however, to implement a general-case version that uses vert/horiz as a ratio and increment controls the speed directly.

Another flaw in LSMP is that it always calls setPosition, and hence always marks the Sprite as dirty. Again, in MPong, this isn't an issue- the ball always moves, so is always dirty and must be rerendered. In other applications, this could have a significant negative performance impact.
Motion - MouseMotionProvider

The MMP also tracks a vert/horiz parameter- these are multipliers to control the rate of motion versus the mouse. In most applications, these should be either zero or one- in MPong, the vert component is always 0, horiz is always 1. Other values can be used- for example, setting horiz to 2 means that for each pixel the mouse moves, the sprite would move 2.

We may not want the sprite directly attached to the mouse- hence the offset parameter. Offset just specifies an x/y offset that can be any value. In MPong this is used as a "fudge" factor to make sure the Sprite lines up better with the mouse.

This is one of the buggier segments in the Animator API. It's difficult to get the movements tracked properly- if the mouse moves too fast, the Browser might drop some MouseMove events- a critical problem if the mouse is moving out of the bounds of the game area. Hence fudge factors like "offset". Before the MMP starts providing, "registerMouse" must be called- in MPong, I call it relative to the BODY of the document- so all motions must be tracked that way. I attempted to tie it to MouseOut events as well- which would have allowed me to track it within the game area instead, but that just didn't work. Since MMP relies on the client positions and translates them to the containing element, this isn't a problem.

Really though, MMP needs to be re-written before production use. It works well enough, but I wouldn't trust it not to break down if someone starts to do anything interesting with it.

clock.js


The Clock is very simple. It simply uses "setInterval" to schedule repeated callbacks to its own "fireEvent" method (imported via the EventSource Aspect). This is a nice event-driven clock, which makes it easy to tie the game functionality into it. There's not much there.

Game


MPong is a test-case for the Animator API. While a very simple game, it provides enough complexity to give a reasonable test of the Animator API. A few moments of play will reveal that it's not much as games go- dull, uninteresting without an uneven challenge level (the ball sometimes crawls, or builds up to ridiculous speeds). Despite this, it does a good job of demonstrating a realistic use of the Animator API, as well as some applicable design patterns.

Paddles


Paddles are merely raw Sprites. They are tied to an instance of the MouseMotionProvider class, and hence follow the mouse. There's nothing really exciting here.

Ball


The Ball is a bit more interesting. There's a few layers here- the Ball exhibits some complex behavior. It needs to bounce, detect when it's fallen out of bounds into the scoring area, etc. The bottom layer is the Sprite used to represent the ball. A few areas of the game directly interact with the Sprite- the Pong class is directly responsible for triggering its rendering process, for example. Instead of weighing down the implementation of the Ball with inheritance, I simply tracked two objects- the ballSprite and an instance of the Ball class. There's no strong relationship between these classes at all- the Ball simply wraps around any Sprite and provides Ball behaviors. It does not alter the prototype or the instance- it just operates on the Sprite's properties. In this, it's similar to the Decorator pattern, but without the strong ownership implied by the pattern.

The Ball class owns a reference to the ballSprite, as well as its own private LSMP instance. Remember, LSMP works for any linear motion that can be represented as a constant vector. The Ball delegates all motion tracking to this provider and contains all the bounds-checking logic in itself. After letting the LSMP tell the Sprite where to go, the Ball checks that position- if the Sprite is impacting on one of the boundary walls (Left/Right), it reverses the sign on the LSMP's horiz vector component- "reflecting" the ball. If the ballSprite impacts on a paddle, it does the same to the vert component- again, "reflecting" the ball. Finally, if the ball hits a scoring area, it fires an event to announce this. It's the responsibility of the game to handle reseting the board. At the moment, I'm double checking the bounds, since it checks the bounds for every paddle. Again, for this application, that's not a problem, but with a large number of potential collisions, that could become very inefficient.

Pong


With all this functionality pushed into classes, all the Pong class really needs to do is make sure it's providing the key plumbing. It tracks a Clock, which tells it to render. It calls out to the Sprites, the Ball, etc. for all the logic. It simply needs to track the "globals" like the current score, register events, and so on.

Problems


In addition to a few of the problems I've discussed above, there's a few more serious flaws. It appears that on the first viewing of the page, the bottom paddle falls outside of the gaming area. Strangely, a refresh fixes that. Why? Why would a refresh even do anything? I'll have to track down exactly what's wrong there.

There's also something wrong in the Ball motion logic- specifically something having to do with bouncing off the right wall- sometimes, and for no reason that I can determine (yet), it just sticks to the right wall and stops. I think it happens when the ball is moving too fast on the horizontal axis.

The final issue isn't my fault, I swear. I left an early version of the page running in Firefox overnight. After 16 hours- it leaked memory like sieve. My system (Windows 2000- yes, I know) came down hard. There's really nothing I can do about that, and I can't imagine anyone playing this for 16 hours- but it's worth noting that it happens.
  • Um -- I tried to play it, but I didn't have a ball.

    The scores went up though ^_^
    • Interesting. Try giving it a refresh- there's a few bugs that (for some inexplicable reason) get cleared out by doing that. Failing that- which browser are you using? I've tested in Firefox 2(Windows and Mac) and IE 6. It probably won't work in anything else.
Powered by LiveJournal.com