GEOWars
Game Engine + Geometry Wars + Level Editor
Install / Use
/learn @ThirdCommand/GEOWarsREADME
GEOWars
Enjoy Now! https://thirdcommand.github.io/GEOWars/
GEOWars was inspired by games like Galaga, Asteroids, and Geometry Wars. The game can be played with a keyboard or a controller. The player controls a ship moving inside an arena where enemy shapes appear. The goal is to shoot down as many of these enemies as possible without getting hit by an enemy shape. When enemies are shot, they explode with vibrant color particle effects making the game visually spicy.
Game Engine
I created a framework for JavaScript programmers to create simulations and games for Canvas. Users can add game objects and give them properties such as physics, sprites, colliders and colors. Users can dynamically mutate these properties through life cycle methods and manage their game through the use of a built in game script.
Life Cycle
- Check Collisions
- Move physics components
- Update game objects
- Render sprites at their new locations
- Update game script
- Play Sounds
Game Object
To make a GameObject, extend the class with GameObject. This will provide a ton of built in functionality. To create the GameObject, you must pass a reference to the game engine into into the super. Every frame, all of the GameObjects that were added will be updated by calling the method Update on them. This method can be overwritten by the user in the classes that inherit from GameObject, providing the user a place to write code that they want the GameObject to run every frame.
I utilize update to allow the Grunt enemies to chase after the player, and animate their sprite:
class Grunt extends GameObject {
constructor(engine, pos, shipTransform) {
super(engine)
this.transform.pos = pos
this.exists = false;
this.stretchDirection = -1;
this.shipTransform = shipTransform
this.radius = 5;
this.points = 70
this.spawnSound = new Sound("sounds/Enemy_spawn_blue.wav", 0.5);
this.playSound(this.spawnSound)
this.addLineSprite(new GruntSprite(this.transform))
this.addChildGameObject(new EnemySpawn(this.gameEngine))
}
...
update(timeDelta) {
if (this.spawned) {
this.chase(timeDelta)
let cycleSpeedScale = timeDelta / NORMAL_FRAME_TIME_DELTA;
let cycleSpeed = 0.01;
if (this.lineSprite.stretchScale_W < 0.7 || this.lineSprite.stretchScale_W > 1) {
this.stretchDirection *= -1
}
this.lineSprite.stretchScale_W = this.lineSprite.stretchScale_W + -this.stretchDirection * cycleSpeed * cycleSpeedScale;
this.lineSprite.stretchScale_L = this.lineSprite.stretchScale_L + this.stretchDirection * cycleSpeed * cycleSpeedScale;
if (this.gameEngine.gameScript.isOutOfBounds(this.transform.absolutePosition(), this.radius)) {
this.wallGraze()
}
}
chase(timeDelta) {
let speed = 1.5
let shipPos = this.shipTransform.absolutePosition();
let pos = this.transform.absolutePosition()
let dy = shipPos[1] - pos[1];
let dx = shipPos[0] - pos[0];
const velocityScale = timeDelta / NORMAL_FRAME_TIME_DELTA;
let direction = Math.atan2(dy, dx);
pos[0] += speed * Math.cos(direction) * velocityScale
pos[1] += speed * Math.sin(direction) * velocityScale
}
}
The class methods that are available to the user when inheriting from GameObject include:
#addPhysicsComponent#addLineSprite#addCollider#addChildGameObject#playSound#addMousePosListener#addLeftControlStickListener#addRightControlStickListener#remove
addCollider
Takes in the following parameters
String: collider name
Reference to GameObject: (usually "this")
Double: hitbox radius
Array: names of the GameObjects it can hit (subscriptions)
Array: names of the types of colliders it can hit
With these parameters, a user can create a custom collider that can subscribe to specific game objects and their specific colliders. An example where this is necessary is with things that try to dodge, like the Weaver in my game. The Weaver tries to dodge bullets when they are within a certain distance, but explodes when in direct contact. The Weaver's "BulletDodge" collider handles dodging the bullets, and the Bullet's "bulletHit" collider handles the direct contact.
The "BulletDodge" collider is subscribed to the Bullet's "General" collider, and the "bulletHit" collider is subscribed to the Weaver and every other enemy type's "General" collider. I picked the name "General" because it is what a new collider type can use to add functionality to every GameObject while only writing the code in one place.
When a collision between the game object and an object that it is subscribed to occurs, onCollision will be called on the game object. Two parameters are passed into onCollision: The collider it contacted, and the type of collider that was triggered. The function exists in the parent class GameObject and is waiting to be overwriten.
Bullet Collision Code:
class Bullet extends GameObject {
...
addBulletColliders(){
let subscriptions = ["Grunt", "Pinwheel", "BoxBox", "Arrow", "Singularity", "Weaver"]
this.addCollider("bulletHit", this, this.radius, subscriptions, ["General"])
this.addCollider("General", this, this.radius)
}
...
onCollision(collider, type){
if (type === "bulletHit") {
let hitObjectTransform = collider.gameObject.transform
let pos = hitObjectTransform.absolutePosition()
let vel = hitObjectTransform.absoluteVelocity()
let explosion = new ParticleExplosion(this.gameEngine, pos, vel)
collider.gameObject.remove()
}
}
...
}
And the Weaver collision code:
class Weaver extends GameObject {
...
addColliders(){
this.addCollider("General", this, this.radius)
this.addCollider("BulletDodge", this, this.weaverCloseHitBox, ["Bullet"], ["General"])
}
...
onCollision(collider, type){
if (type === "BulletDodge") {
this.acceptBulletDirection(collider.gameObject.transform.pos)
}
}
...
}
addLineSprite
A LineSprite contains a transform and a #draw function for the user to overwrite. It is where the user puts the commands for the canvas context to draw a GameObject. #addLineSprite adds the sprite to the list of sprites to be rendered by the game engine during its rendering stage of the frame cycle. The following is an example of adding a LineSprite to a game object:
import {LineSprite} from "../../../game_engine/line_sprite";
class SingularitySprite extends LineSprite {
constructor(transform, spawningScale = 1) {
super(transform)
this.spawningScale = spawningScale
this.throbbingScale = 1
this.radius = 15;
this.spawned = false;
}
draw(ctx) {
let spawningScale = this.spawningScale
if (this.spawned) {
spawningScale = this.throbbingScale
}
ctx.strokeStyle = "#F173BA"
let r = 95;
let g = 45;
let b = 73;
ctx.save();
let blurFactor = 0.5
ctx.shadowColor = "rgb(" + r + "," + g + "," + b + ")";
ctx.shadowBlur = 10
ctx.strokeStyle = "rgba(" + r + "," + g + "," + b + ",0.2)";
ctx.lineWidth = 7.5;
this.drawSingularity(ctx, this.radius * spawningScale);
ctx.lineWidth = 6
this.drawSingularity(ctx, this.radius * spawningScale);
ctx.lineWidth = 4.5
this.drawSingularity(ctx, this.radius * spawningScale);
ctx.lineWidth = 3
this.drawSingularity(ctx, this.radius * spawningScale);
ctx.strokeStyle = 'rgb(255, 255, 255)';
ctx.lineWidth = 1.5
this.drawSingularity(ctx, this.radius * spawningScale);
ctx.restore();
// ctx.lineWidth = 2;
// drawSingularity(ctx, this.radius * spawningScale);
}
drawSingularity(ctx, radius) {
ctx.beginPath();
let pos = this.transform.absolutePosition()
ctx.arc(pos[0], pos[1], radius, 0, 2 * Math.PI, true);
ctx.stroke();
}
}
class Singularity extends GameObject {
constructor(engine, pos) {
super(engine)
this.transform.pos = pos;
...
this.addLineSprite(new SingularitySprite(this.transform))
this.lineSprite.throbbingScale = 1
}
playSound
A Sound is another object built into the game engine that allows users to give it a url and a volume. Sound has methods #play, #mute, #unmute, #pause, and #toggleMute. playSound allows the user to add a sound to the game engine sound queue to be played. Duplicate sounds added during the same frame will not change in volume, as the game engine will only play one of them. Sound's constructor takes in the URL and volume (a bool, zero to one) as parameters.
class Arrow extends GameObject {
constructor(engine, pos, angle = Math.PI / 3) {
super(engine)
this.transform.pos = pos
...
this.spawnSound = new Sound("GEOWars/sounds/Enemy_spawn_purple.wav", 0.5)
this.playSound(this.spawnSound)
}
...
}
addPhysicsComponent
this function creates a new physics component for the game engine to update every frame. The position, velocity, and acceleration are stored in a GameObject's transform. Physics components take in the change in time between frames to update the position and velocity of the GameObject.
Controls Event Listeners
#addMousePosListener#addLeftControlStickListener#addRightControlStickListener
these methods add event listeners for their respective names. These are the only ones I needed for my game so they were the first to be added. the rest of the controller buttons and keyboard input will be added in later versions.
addMousePosListener will add this GameObject to the list of objects that updateMousePos(pos) will be called on. Add that method to your Ga
