2D Game Engine Development in HTML5
Textbook readings
- Chapter 10 - Capturing User Input
- Chapter 14 - Adding Physics with Box2D
- Chapter 15 - Canvas Transformation Matrix
- Chapter 16 - Building Animation Maps, Adding a Canvas Viewport
Camera Component - Code
The camera is a component that gets added to a Stage entity
Camera System Features
- Before a scene iterates over it's objects to draw, the camera updates the Canvas transform properties
- After a scene draws, it then restores the Canvas transform properties.
- Can be told to follow an entities (GameObject/Sprites). Each frame it will set it's own position to match the following entity.
- NOTE: Because our scene now has transformation properties, we will need to account for that regarding mouse coordinates.
Input Module - Code
The Input module takes care of wrapping keyboard, mouse, touch, and gamepad events, and connecting them to our game
Input System Features
- Enable devices seperately: Game code can say whether it's using mouse, keyboard, touch, or gamepad. Would be a good candidant for components!
- Can stores a mapping of action events ("fire", "jump", "move") to device events ("mousemove", "keydown", etc) to create an abstraction so controls can be changed easily.
I haven't finished this part myself. Just so much to do!
- Game code can register a callback for action events or input events, or just poll the input system for values.
- Gamepad support isn't finalized on all browsers: polling on Chrome, connection callbacks on Firefox.
Animation Module - Code
Another layer of data abstraction
Sprite Sheets are great at storing multiple images in a single file, and also multiple frames of the same sprite
We need a way to say which of those frame are used for running, and which are for walking, and which are for standing, etc
And other properties along with frames: how fast is the running playback, any events to trigger, whether to loop or transition into a next animation, etc
Animation System Features
- Add/get AnimationData - an object representing an animation set (storing a number of different animation sequences). The engine associated this data with a particular sprite name.
- Animation Component extends any sprite entity using a SpriteSheet. In a nutshell, it drives the .frame property based on current animation and playback speed.
- Sprites have a .sheetName property to say which tile(s) from a sprite sheet to use, and a .animSetName property that says which animation set it uses. Using different properties allows us to apply the same animation sets to a different sprite.
- Sends events for when an animation ends, loops, and on each frame. (You could register for frame 3 of a fire animation to spawn a special effect!)
2D Physics - Overview
The Blockbreak demo game does pixel-perfect collision checks with bounding boxes, but doesn't support rotations or have great collision reactions.
We could program equations of motion for each object as needed (ie, arrows, bullets), but a more general solution is ideal.
If we want our game to have a variety of collision shapes, robust collision response (bouncing, sliding, rotations), and more believable movement, then we need a Physics Engine.
Pros
- Re-usability, rapid prototyping and iteration
- Can also be used to drive special effects or animations
- Once you get Rigid-Bodies, we can start working with water, wind, clothes, etc.
Cons
- A general-purpose engine can be quite processor intensive.
- Because it's a general system, we have to feed it a lot of data from the game. Can be hard to get behaviors to work as desired (Half-Life: pushing crates)
Desired System Features
- Create a physics system (world) that manages all of the interactions (dynamic motion and contact resolution) between Rigid bodies (actors) (no soft-bodies)
- Factory for creating new rigid bodies and adding them into the world
- Collision filtering, so we can group which objects that can collide with each other
- Collision notifications, so we can have gameplay respond to contacts
- Picking: QueryAABB, QueryPoint, QueryShape, RayCast, RayCastAll, RayCastOne
- Can create constraints (joints) between bodies
- Debug rendering
Mathematics!
Linear motion
- Very similiar in 2D and 3D!
- position is a vector, representing a 2D point in space.
- velocity = change in position over time (meters per second, m/s)
- acceleration = change in velocity over time (meters per second per second, m/s^s)
- Assume constant acceleration, because otherwise our math will get too hard!
Angular motion
- Thank goodness we're in 2D!
- orientation is the direction that the object is facing, represening a 2D angle (scalar)
- A vector orientation representing the facing direction - { x: cos(angle), y: sin(angle) }
- rotation is a change in orientation (to rotate an object is to change it's angle)
- angular velocity = the rate of change in orientation (radians per second, also a scalar)
- angular acceleration = the rate of change in angular velocity (radians per second per second, also a scalar)
- Radians are unitless!
- Center of Mass / Center of Gravity / Origin
- Objects is in many locations at the same time: it covers some extended area.
- Locations on the object are given relative to the origin of the object, so that it's the same regardless of rotation (car headlight is ahead of the center of car). Equations for converting a relative point to an absolute point is called a "transformation from local space to world space"
- When the center of mass is the object's origin, then physical behavior can be decomposed into the linear motion of the center of mass, and the angular motion around the same point. (Involved proof, I'll explain later)
Newton's Laws of Motion
- 1) An object with no other forces will maintain velocity
- Technically he said momentum, which is the product of velocity and mass. Since mass is constant for linear movement, we can just work with velocity. For angular movement we'll have to worry about this.
- In the real world there are forces slowing down moving objects (eg. air drag), which we simulation using damping. Each frame when we integrate, we will remove a portion of the object's velocity to simulate drag. 0 means no velocity is saved, 1 means all is saved.
- 2) The acceleration of an object is calculated by the net forces acting on it and the object's mass: F = ma
- Two objects experiencing the same force will behave differently depending on their mass
- If we want to drive a game object by forces, then we need to integrate: a = F/m.
- If an object has 0 mass then we'd have division by zero, so use that to imply that the object has infinite mass, meaning we do not bother calculating it's motion based on forces.
- D'alembert's principle tells us (through grand simplicification) that we can replace a set of forces with a single force that is the sum of all forces.
- Gravity is funny: calculation is based on both the mass of the attractor and attractee. Since the earth is so huge, the difference in mass for our rigid bodies is neglitable, so we can simulate gravity as an acceleration.
- 3) For each action there's an equal and opposite reaction. When a body collides with another body, we need to calculate the amount of force that occured in the collision so both bodies can react.
- A lot of the difficulty is determining where two objects touch or are connected.
- We can use 'contact forces' to keep objects from interpenetrating.
Solving the equations of motion backwards, Integral Calculus!
- We use an "integrator" to update the position and velocity of each object.
- Given a time interval over which to update an object: position = position + velocity * dt + acceleration * (dt^2 / 2)
- Known as Euler's Equations of motion. It's not very accurate and generally not used. We have Runge Kutta, Verlet, and few more to choose from.
- Further reading: Integration Basics
- Further reading: How important fixed timesteps are
Box2Dweb to the rescue
- Box2dweb Download - port of Box2DFlash, which is in turn a port from C++
- Box2d Documentation - Flash version, but it matches up
- Con: Box2D creates lots of objects, which is bad for JS garbage collection
Physics Module - Code
Add physics functionality through components: one for the stage representing the physics world, and one for our sprites representing rigid bodies
Default properties are stored ahead (helpful when referencing what parameters we can set when writing game code)
PhysicsWorldComponent
- PhysicsWorld is a component that we add to our stages
- Creates a Box2D world, which takes care of collision detection and response
- Call step() every frame, tell Box2D to run a number of substeps (velocity and position iterations). More iterations = more stable, but at the cost of rendering time
- Uses a Box2D Listener to get notified whenever there is a collision
- Factory for creating new rigid bodies (used by PhysicsActorComponent)
- Factory for creating new joints between bodies (API not finalized)
- Utility functions for doing overlap and sweep test, helpful for picking (API not finalized)
- Debug functions for drawing the physics world to our canvas
PhysicsActorComponent
- PhysicsActor is a component that we can add to our sprites
- After the entity is added to a scene (which we have to wait for because the scene holds the Box2D world), it creates a Box2D Rigid Body with the given properties like:
- bodyType - "dynamic" (free-moving rigid-body, our default), "static" (infinite mass, doesn't move), "kinematic" (can move to cause collisions but doesn't react to any itself)
- mass - overrides the density property for the fixture
- allowSleep - can prevent an object from falling asleep (sleeping objects do not generate contacts)
- linearDamping - use to reduce the linear velocity.
- angularDamping - use to reduce the angular velocity.
- fixedRotation - does not rotate
- bullet - prevent physics tunneling (expensive)
- The body is assigned a shape (Box, Circle, Polygon) to define collision bounds (Fixture) that must be convex (bodies can have multiple fixtures to build concave shapes) using properties like:
- density - the distribution of mass across the entire collision shape
- friction - the friction coefficient, usually in the range [0,1]. Applied to both how it picks up speed while in contact, and loses speed while in contact.
- restitution - bounciness, how much velocity is conserved after a collision
- isSensor - does not have solid collision, but will still get notifications when it touches another object.
- shape - Box2D supports "circle", "block" and "polygon". Different configuration parameters for each type
- Box2D body has userData, a pointer we can assign to whatever we want. In this case we'll point back to the entity, so that our collision callbacks can know what sprites are part of the collision.
- Each frame, after the physics world has updated, each actor component updates the parent sprite entity's position and rotation.
- Utility functions for getting/changing properties: velocity,
- Utility functions for adding forces, impulse.
API Concerns: When do we need to account for world scale? Prepare for bugs related to this.
API Alternatives: Could the Box2D world live on the engine and not on a scene?
API Alternatives: The actor component could extend some of the utility functions onto the entity?