xxxxxxxxxx
681
/*
Final project for Introduction to Interactive Media.
*/
const WIDTH = 576;
const HEIGHT = 576;
const TILE_W = 64;
const TILE_H = 64;
const FRAME_RATE = 60;
let gameEngine;
function preload() {
gameEngine = new GameEngine();
}
function setup() {
frameRate(FRAME_RATE);
pixelDensity(1);
createCanvas(WIDTH, HEIGHT);
gameEngine.run();
}
function draw() {
gameEngine.update();
}
function keyPressed() {
gameEngine.sUserInput(key.toLowerCase(), keyCode, "START");
}
function keyReleased() {
gameEngine.sUserInput(key.toLowerCase(), keyCode, "END");
}
function readSerial(data) {
if (data != null) {
gameEngine.readArduino(data);
}
}
class GameEngine {
constructor() {
// this.assets = new Assets(assetsFile);
this.sceneMap = new Map();
this.currentScene = null;
this.running = false;
this.isConnectedArduino = false;
}
run() {
// this.assets.loadQueuedAnimations();
this.running = true;
this.changeScene("MENU", new Scene_Menu(this));
}
changeScene(sceneName, scene, endCurrentScene) {
if (scene != null) {
this.sceneMap.set(sceneName, scene);
} else {
const foundScene = this.sceneMap.get(sceneName);
if (foundScene == null) {
print(`Warning: Scene does not exist: ${sceneName}`);
return;
}
}
if (endCurrentScene) {
if (this.currentSceneName) {
this.sceneMap.delete(this.currentSceneName);
}
}
this.currentSceneName = sceneName;
}
update() {
if (!this.running || this.sceneMap.size == 0) return;
// Update Arduino connection state.
this.isConnectedArduino = serialActive;
// this.isConnectedArduino = true;
// Update current scene.
const currentScene = this.sceneMap.get(this.currentSceneName);
currentScene.update();
currentScene.sRender();
}
quit() {
this.running = false;
}
sUserInput(key, keyCode, type) {
if (!this.running) return;
this.sceneMap.get(this.currentSceneName).sDoAction(key, keyCode, type);
}
connectArduino() {
setUpSerial();
}
readArduino(data) {
if (this.running) {
if (this.isConnectedArduino) {
this.sceneMap.get(this.currentSceneName).sReadArduino(data);
} else {
}
} else {
}
}
sendArduino(data) {
writeSerial(data);
}
}
class Scene_Menu {
constructor(game) {
this.game = game;
}
update() {}
sRender() {
rectMode(CORNERS);
background(0);
noStroke();
fill(255);
textAlign(LEFT);
textStyle(BOLD);
textSize(36);
text("Box Ball Musician", 60, 100, WIDTH - 100, 200);
textStyle(NORMAL);
textSize(22);
if (!this.game.isConnectedArduino) {
text("Press SPACE to connect to Arduno.", 60, 300, WIDTH - 60, 400);
} else {
const instructions =
"Instructions:\n" +
"- Move ball around box.\n" +
"- Follow instructions.\n\n" +
"Press SPACE to begin!";
text(instructions, 60, 300, WIDTH - 60, 400);
}
}
sDoAction(key, keyCode, type) {
if (type == "END") return;
if (key == " ") {
if (!this.game.isConnectedArduino) {
this.game.connectArduino();
} else {
this.game.changeScene("PLAY", new Scene_Play(this.game));
}
}
}
sReadArduino(data) {
print("Got data", data);
this.game.sendArduino("1\n");
}
}
class Scene_Play {
constructor(game) {
this.game = game;
this.currentFrame = 0;
this.entityManager = null;
this.triggerStatus = null;
this.drawShapes = true;
this.drawCollision = false;
this.drawTextBoxes = true;
this.tileSize = createVector(TILE_W, TILE_H);
this.loadLevel();
}
update() {
this.entityManager.update();
this.sMovement();
this.sCollision();
this.sTriggers();
this.currentFrame++;
}
onEnd(type) {
type = type === undefined ? "exit" : type;
if (type == "exit") {
this.game.changeScene("MENU", null, true);
} else if (type == "finish") {
this.game.changeScene("END", new Scene_End(this.game), true);
}
}
loadLevel() {
this.entityManager = new EntityManager();
this.triggerStatus = new Map();
const grid = [
"xxxxxxxxx",
"x.......x",
"x.1.2.3.x",
"x.......x",
"x.4.p.6.x",
"x.......x",
"x.7.8.9.x",
"x.......x",
"xxxxxxxxx",
];
const gridY = grid.length;
const gridX = grid[0].length;
for (let y = 0; y < gridY; y++) {
for (let x = 0; x < gridX; x++) {
const type = grid[y][x];
if (type == "x") {
// A wall.
const e = this.entityManager.addEntity("tile");
e.c.shape = new CShape("rect", this.tileSize, color(150, 0, 150));
e.c.bbox = new CBoundingBox(this.tileSize, true);
e.c.transform = new CTransform(this.getPosition(x, y, e));
} else if (type == "p") {
// The player.
const size = createVector(48, 48);
const e = this.entityManager.addEntity("player");
e.c.shape = new CShape("ellipse", size, color(50, 200, 50));
e.c.bbox = new CBoundingBox(size, false);
e.c.transform = new CTransform(this.getPosition(x, y, e));
e.c.input = new CInput();
} else if ("1" <= type && type <= "9") {
// A trigger.
const e = this.entityManager.addEntity("trigger");
e.c.shape = new CShape("rect", this.tileSize, color(30, 70, 150));
e.c.bbox = new CBoundingBox(this.tileSize, false);
e.c.transform = new CTransform(this.getPosition(x, y, e));
const triggerId = parseInt(type);
e.c.trigger = new CTrigger(triggerId);
this.triggerStatus.set(triggerId, { start: null, end: null });
} else if (type == ".") {
} else {
print(`Unknown type ${type} at (${x},${y})`);
}
}
}
// Main text box.
{
const e = this.entityManager.addEntity("textbox");
e.c.shape = new CShape(
"rect",
this.tileSize.copy().mult(2),
color(50, 50, 50)
);
e.c.transform = new CTransform(createVector(width / 2, height / 2));
e.c.text = new CText("Starting...");
}
}
sMovement() {
for (const player of this.entityManager.get("player")) {
const transform = player.c.transform;
const input = player.c.input;
transform.prevPos.set(transform.pos);
const vel = transform.velocity;
vel.set(0, 0);
// Arrow key input. For debugging.
if (input.left) vel.x -= 5;
if (input.right) vel.x += 5;
if (input.up) vel.y -= 5;
if (input.down) vel.y += 5;
// Sensor input.
vel.x += input.sensorX;
vel.y += input.sensorY;
transform.pos.add(vel);
}
}
sCollision() {
// Player-tile collisions.
for (const player of this.entityManager.get("player")) {
const transform = player.c.transform;
for (const tile of this.entityManager.get("tile")) {
if (!tile.c.bbox.blockMove) continue;
const overlap = Physics.GetOverlap(player, tile);
if (!(overlap.x > 0 && overlap.y > 0)) {
// No overlap.
continue;
}
const prevOverlap = Physics.GetPreviousOverlap(player, tile);
let shouldPushBackX = false;
let shouldPushBackY = false;
if (prevOverlap.x > 0 && prevOverlap.y > 0) {
shouldPushBackX = true;
shouldPushBackY = true;
} else if (prevOverlap.y > 0) {
shouldPushBackX = true;
} else if (prevOverlap.x > 0) {
shouldPushBackY = true;
} else {
if (overlap.x > overlap.y) {
shouldPushBackY = true;
} else {
shouldPushBackX = true;
}
}
if (shouldPushBackX) {
// If entity is to the left of the tile, subtract overlap; add it otherwise.
const isToLeft = transform.pos.x < tile.c.transform.pos.x;
transform.pos.x += isToLeft ? -overlap.x : overlap.x;
}
if (shouldPushBackY) {
// If entity is below the tile, subtract overlap; add it otherwise.
const isAbove = transform.pos.y < tile.c.transform.pos.y;
transform.pos.y += isAbove ? -overlap.y : overlap.y;
}
}
}
// Player-trigger collisions.
for (const player of this.entityManager.get("player")) {
for (const trigger of this.entityManager.get("trigger")) {
if (!trigger.active) continue;
const overlap = Physics.GetOverlap(trigger, player);
if (!(overlap.x > 0 && overlap.y > 0)) continue;
const triggerId = trigger.c.trigger.id;
// Update activation times for this trigger.
const status = this.triggerStatus.get(triggerId);
if (status.end == null || status.end != this.currentFrame - 1) {
// Either never activated before, or activated but not currently.
status.start = this.currentFrame;
}
status.end = this.currentFrame;
}
}
}
sTriggers() {
for (const [id, status] of this.triggerStatus.entries()) {
if (status.end == this.currentFrame) {
// print("Triggering", id);
}
}
}
sRender() {
function renderShape(e) {
const transform = e.c.transform;
if (transform == null) return;
const shape = e.c.shape;
if (shape == null) return;
fill(shape.color);
if (shape.type == "rect") {
rect(transform.pos.x, transform.pos.y, shape.size.x, shape.size.y);
} else if (shape.type == "ellipse") {
ellipse(transform.pos.x, transform.pos.y, shape.size.x, shape.size.y);
}
}
background(0);
rectMode(CENTER);
// Draw text boxes.
if (this.drawTextBoxes) {
textAlign(CENTER);
noFill();
noStroke();
for (const e of this.entityManager.get("textbox")) {
renderShape(e);
const pos = e.c.transform.pos;
const size = e.c.shape.size;
fill(255);
text(e.c.text.text, pos.x, pos.y);
}
}
if (this.drawShapes) {
noStroke();
// Draw everything with a shape.
for (const e of this.entityManager.get("tile")) {
renderShape(e);
}
for (const e of this.entityManager.get("trigger")) {
renderShape(e);
}
for (const e of this.entityManager.get("player")) {
renderShape(e);
}
}
if (this.drawCollision) {
// Draw collision boxes.
noFill();
strokeWeight(1);
for (const e of this.entityManager.entities) {
const bbox = e.c.bbox;
if (bbox == null) continue;
const transform = e.c.transform;
stroke(255);
if (bbox.blockMove) stroke(255, 0, 0);
rect(transform.pos.x, transform.pos.y, bbox.size.x, bbox.size.y);
}
}
}
sDoAction(key, keyCode, type) {
const p = this.entityManager.get("player")[0];
if (type == "START") {
if (keyCode == ESCAPE) this.onEnd("exit");
else if (key == "s") this.drawShapes = !this.drawShapes;
else if (key == "c") this.drawCollision = !this.drawCollision;
else if (keyCode == UP_ARROW) p.c.input.up = true;
else if (keyCode == LEFT_ARROW) p.c.input.left = true;
else if (keyCode == DOWN_ARROW) p.c.input.down = true;
else if (keyCode == RIGHT_ARROW) p.c.input.right = true;
} else if (type == "END") {
if (keyCode == UP_ARROW) p.c.input.up = false;
else if (keyCode == LEFT_ARROW) p.c.input.left = false;
else if (keyCode == DOWN_ARROW) p.c.input.down = false;
else if (keyCode == RIGHT_ARROW) p.c.input.right = false;
}
}
sReadArduino(data) {
// print("Got data", data);
// Turn x and y from [0, 1] to [-3, 3].
const [x, y] = data.trim().split(",");
let sensorX = map(x, 0, 1, 3, -3);
let sensorY = map(y, 0, 1, -3, 3);
// If absolute value is this much, it probably means that the
// ball is not in the box. So set values to 0.
if (abs(sensorX) == 3) {
sensorX = 0;
}
if (abs(sensorY) == 3) {
sensorY = 0;
}
// Update input components of all players.
for (const player of this.entityManager.get("player")) {
const input = player.c.input;
input.sensorX = sensorX;
input.sensorY = sensorY;
}
// Debug printing.
if (this.currentFrame % 10 == 0) {
print("sensor", sensorX, sensorY);
}
// Send answer to arduino.
this.game.sendArduino("2\n");
}
getPosition(tileX, tileY, entity) {
// Return the midpoint pixel coordinates of some tile in some room.
const entitySize =
entity.c.shape != null
? entity.c.shape.size
: entity.c.bbox != null
? entity.c.bbox.size
: this.tileSize;
return createVector(
tileX * this.tileSize.x + entitySize.x / 2,
tileY * this.tileSize.y + entitySize.y / 2
);
}
}
class EntityManager {
// The EntityManager class manages all entities in the game,
// i.e. their creation and destroyal every frame.
constructor() {
this.entities = [];
this.entitiesToAdd = [];
this.entityMap = new Map();
this.totalEntities = 0;
}
removeDeadEntities(lst) {
return lst.filter((it) => it.active);
}
update() {
// First, remove all dead entities.
this.entities = this.removeDeadEntities(this.entities);
for (const [k, v] of this.entityMap) {
this.entityMap.set(k, this.removeDeadEntities(v));
}
// Then, add all entities that should be added.
if (this.entitiesToAdd.length > 0) {
for (const e of this.entitiesToAdd) {
this.entities.push(e);
this.get(e.tag).push(e);
}
this.entitiesToAdd = [];
}
}
addEntity(tag) {
// When adding an entity, add it to the list, so it is
// truly added when the time is appropriate.
const e = new Entity(this.totalEntities++, tag);
this.entitiesToAdd.push(e);
return e;
}
get(tag) {
if (!this.entityMap.has(tag)) {
this.entityMap.set(tag, []);
}
return this.entityMap.get(tag);
}
}
class Entity {
constructor(id, tag) {
this.id = id;
this.tag = tag;
this.active = true;
this.c = new ComponentList();
}
destroy() {
this.active = false;
}
}
class ComponentList {
// A list of components that every entity can have, although they can be null.
constructor() {
this.transform = null;
this.bbox = null;
this.shape = null;
this.input = null;
this.trigger = null;
this.input = null;
this.text = null;
}
}
class CTransform {
/*
Has:
- The entity's current position.
- The entity's position in the previous frame.
- The entity's velocity.
*/
constructor(pos, velocity) {
this.pos = pos === undefined ? createVector(0, 0) : pos.copy();
this.prevPos = this.pos.copy();
this.velocity =
velocity === undefined ? createVector(0, 0) : velocity.copy();
}
}
class CBoundingBox {
/*
Has:
- The collision bounding box size.
- Whether it should block movement.
*/
constructor(size, blockMove) {
this.size = size === undefined ? createVector(0, 0) : size.copy();
this.halfSize = this.size.copy().div(2);
this.blockMove = blockMove;
}
}
class CShape {
/*
Contains information for displaying the entity:
- Type: square or circle.
- Size: size, a vector.
- Color: color, a p5 color object.
*/
constructor(type, size, color) {
this.type = type;
this.size = size.copy();
this.color = color;
}
}
class CInput {
// Keeps track of specific properties related to user input.
constructor() {
this.up = false;
this.down = false;
this.left = false;
this.right = false;
this.sensorX = 0;
this.sensorY = 0;
}
}
class CTrigger {
// Tracks trigger id for trigger entities.
constructor(id) {
this.id = id;
}
}
class CText {
constructor(text) {
this.text = text;
}
}
class Physics {
static GetOverlap(a, b) {
// Get overlap amount between two entities.
const ta = a.c.transform;
const tb = b.c.transform;
const ca = a.c.bbox;
const cb = b.c.bbox;
return createVector(
ca.halfSize.x + cb.halfSize.x - abs(ta.pos.x - tb.pos.x),
ca.halfSize.y + cb.halfSize.y - abs(ta.pos.y - tb.pos.y)
);
}
static GetPreviousOverlap(a, b) {
// Get overlap amount between two entities, from the previous frame.
const ta = a.c.transform;
const tb = b.c.transform;
const ca = a.c.bbox;
const cb = b.c.bbox;
return createVector(
ca.halfSize.x + cb.halfSize.x - abs(ta.prevPos.x - tb.prevPos.x),
ca.halfSize.y + cb.halfSize.y - abs(ta.prevPos.y - tb.prevPos.y)
);
}
}