2.2.9 Cycle 9 Multiplayer

Design

In this cycle I want to make my game multiplayer so 2 people can enjoy it together.

Objectives

Key functions

Function Name
Use

multiplayer scene

Scene used for the multiplayer version of the game

Development

Outcome

The outcome is a multiplayer mode with two players which take turns to shoot bullets at the other player. When one gets hit, a new terrain is generated and the scoreboard updates.

main.ts
import "kaboom/global"
import kaboom from "kaboom"
import Terrain from "./terrain-generate.js"

export const WIDTH = 1920
export const HEIGHT = 1080
const GRAVITY = 450

kaboom({
  width: WIDTH,
  height: HEIGHT,
  background: [69, 65, 65]
})

/*
const recording = record()

onKeyDown(".", () => {
  recording.download({filename: "recording"})
})
*/

setGravity(GRAVITY)

loadBean()
loadPedit("barrel", "sprites/barrel.pedit")
loadPedit("bullet", "sprites/bullet.pedit")
loadPedit("arrow", "sprites/arrow.pedit")

function addBtn(txt, p, f) {
  // add a parent background object
  const btn = add([
    rect(240, 80, { radius: 8 }),
    pos(p),
    area(),
    scale(1.7),
    anchor("center"),
    outline(4),
  ])

  // add a child object that displays the text
  btn.add([
    text(txt),
    anchor("center"),
    color(0, 0, 0),
  ])

  // onHoverUpdate() comes from area() component
  // it runs every frame when the object is being hovered
  btn.onHoverUpdate(() => {
    btn.scale = vec2(2.04)
    setCursor("pointer")
  })

  // onHoverEnd() comes from area() component
  // it runs once when the object stopped being hovered
  btn.onHoverEnd(() => {
    btn.scale = vec2(1.7)
    setCursor("default")
  })

  // onClick() comes from area() component
  // it runs once when the object is clicked
  btn.onClick(f)

  return btn

}

const addWind = (minWind, windVariance) => {
  //generate random number (+ or -) for wind force
  //display wind 
  //add the force to the bullet either through the move command of the bullet or by detecting when the bullet is high up
  let windForce = minWind + Math.floor(Math.random() * windVariance)
  
  add([
    text(Math.abs(windForce).toString()),
    pos(176, 24)
  ])

  add([
    sprite("arrow"),
    pos(186, 20),
    rotate(windForce < 0 ? 180 : 0),
    anchor("center")
  ])

  return windForce
}

const newBullet = (position, angle, power) => {
  return add([
    sprite("bullet"),
    pos(position),
    //offscreen({ destroy: true }),
    area(),
    anchor("center"),
    body(),
    rotate(angle),
    move(vec2(Math.cos(angle / 180 * Math.PI), Math.sin(angle / 180 *     Math.PI)), (power + 20) * 8.3),              //Calculating movement vector
    "bullet"
  ])
}

/*const player = make([
  sprite("bean"),
  pos(80, HEIGHT - (Terrain.points[80] + 27)),
  rotate(0),
  anchor("center"),
  {
    SPEED_HIGH: 150,
    SPEED_LOW: 45,
    power: 0
  }
])*/

const player = make([
  sprite("bean"),
  pos(),
  area(),
  rotate(0),
  anchor("center"),
  {
    SPEED_HIGH: 150,
    SPEED_LOW: 45,
    ROTATE_MIN: -70,
    ROTATE_MAX: 0,
    POS_MIN: 0,
    POS_MAX: WIDTH / 3,
    power: 0
  }
])

const player2 = make([
  sprite("bean"),
  pos(),
  area(),
  rotate(0),
  anchor("center"),
  {
    SPEED_HIGH: 150,
    SPEED_LOW: 45,
    ROTATE_MIN: 0,
    ROTATE_MAX: 70,
    POS_MIN: 2 * WIDTH / 3,
    POS_MAX: WIDTH,
    power: 0
  }
])

scene("practice", () => {

  onDraw(() => {
    Terrain.drawTerrain()
  })

  Terrain.seedTerrain()
  Terrain.interpolateLinear()
  Terrain.tCollision()
  const wind = addWind(-2, 5)
  
  onLoad(() => {
    add([
      pos(WIDTH / 2, 450),
      text("Press \"esc\" to return to menu"),
      lifespan(2, { fade: 0.5 }),
      anchor("center")
    ])
  })

  const bean = add(player)
  bean.pos = vec2(80, HEIGHT - Terrain.points[80] - 27)
  bean.power = 0
  bean.angle = 0

  const target = add([
    sprite("bean"),
    pos(1500, HEIGHT - (Terrain.points[1500] + 27)),
    area(),
    anchor("center")
  ])

  bean.add([
    sprite("barrel"),
    scale(3, 3),
    pos(0, -60)
  ])

  target.onCollide("bullet", () => {
    addKaboom(target.pos)
    destroy(target)
    destroyAll("bullet")
    wait(1.1, () => {go("practice")})
  })

  onCollide("bullet", "ground", () => {
    destroyAll("bullet")
  })
  
  target.onCollide("nuke", () => {
    addKaboom(target.pos, {scale: 15})
    destroy(target)
    destroyAll("nuke")
    play("explosion")
    wait(3, () => {go("practice")})
  })

  const angle = add([            //UI for displaying the angle of player
    text(Math.floor(bean.angle).toString()),
    pos(24, 24)
  ])

  const power = add([          //UI to display power of the shot
    text(Math.floor(bean.power).toString()),
    pos(100, 24)
  ])

  let bullet
  onKeyDown("space", () => {
    if (get("bullet").length == 0) {
      let endOfBarrel = vec2(bean.pos.x + 85 * Math.cos(-0.15 + bean.angle / 180 * Math.PI), bean.pos.y + 92 * Math.sin(-0.15 + bean.angle / 180 * Math.PI)) //Find the position of the end of the barrel to add bullets
      bullet = newBullet(endOfBarrel, bean.angle, bean.power)
    }
  })

  onUpdate(() => {
    angle.text = Math.abs(Math.floor(bean.angle)).toString()  //Update player angle UI
    power.text = Math.floor(bean.power).toString()

    if (get("bullet").length > 0) {
      if (bullet.pos.x > WIDTH || bullet.pos.x < 0) {
        destroy(bullet)
      }
      if (bullet.pos.y < 300) {
        bullet.pos.x += wind
      }
    }
  })

  let rotationSpeed = bean.SPEED_HIGH

  onKeyDown("alt", () => {
    rotationSpeed = bean.SPEED_LOW  //Slow rotation when holding alt
  })

  onKeyRelease("alt", () => {
    rotationSpeed = bean.SPEED_HIGH  //Normal when alt released
  })

  onKeyDown("a", () => {
    if (bean.pos.x > bean.POS_MIN) {
      bean.pos.x--
      bean.pos.y = HEIGHT - Terrain.points[bean.pos.x] - 27 
    }
  })
  
  onKeyDown("d", () => {
    if (bean.pos.x < bean.POS_MAX) {
      bean.pos.x++
      bean.pos.y = HEIGHT - Terrain.points[bean.pos.x] - 27
    }
  })
  
  onKeyDown("left", () => {
    if (bean.angle > -70) {
      bean.angle += -rotationSpeed * dt() //Rotate left
    }
    else {
      bean.angle = -70  //Stop at 70 degrees anticlockwise
    }
  })

  onKeyDown("right", () => {
    if (bean.angle < 0) {
      bean.angle += rotationSpeed * dt() //Rotate right
    }
    else {
      bean.angle = 0  //Prevent tank rotating the wrong direction
    }
  })

  let smooth = 3;

  onKeyDown("up", () => {
    smooth += 1.2 * dt()
    if (bean.power < 100) {
      bean.power += 0.7 * smooth ** 2 * dt() //Increase power smoothly
    }
    else {
      bean.power = 100  //Maximum power 100
    }
  })

  onKeyRelease("up", () => {
    smooth = 3
  })

  onKeyDown("down", () => {
    smooth += 1.2 * dt()
    if (bean.power > 1) {
      bean.power -= 0.7 * smooth ** 2 * dt()  //Decrease power smoothly
    }
    else {
      bean.power = 0  //Minimum power 0
    }
  })

  onKeyRelease("down", () => {
    smooth = 3
  })

  const cheatInputs = ["up","up","down","down","left","right","left","right","space"]
  let inputs = []
  const cheatCode = (key) => {
    inputs.push(key)
    if (cheatInputs.slice(0,inputs.length).toString() == inputs.toString()) {
      if (inputs.length == cheatInputs.length){
        inputs = []
        dropNuke(0, -300)
      }
    }
    else {
      inputs = []
    }
  }

  const dropNuke = (x, y) => {
    loadSprite("nuke", "/sprites/nuke.png")
    loadSound("explosion", "/sounds/explosion.mp3")
    const nuke = add([
      sprite("nuke"),
      rotate(target.pos.angle(vec2(x,y))),
      scale(0.7),
      pos(x, y),
      anchor("center"),
      area(),
      move(target.pos.angle(vec2(x,y)), 1200),
      "nuke"
    ])
  }
  
  onKeyPress((key) => {
    cheatCode(key)
  })
    
  /*onKeyPress("up", () => {})
  onKeyPress("down", () => {})
  onKeyPress("left", () => {})
  onKeyPress("right", () => {})*/
  
  onKeyDown("escape", () => {
    go("main-menu")
  })

})

scene("multiplayer", (currentTurn = 1, score = [0,0], newLevel = true) => {

  const getEndOfBarrel = (player) => {
    if (player === player_1) {
      return vec2(player.pos.x + 85 * Math.cos(-0.15 + player.angle / 180 * Math.PI), player.pos.y + 92 * Math.sin(-0.15 + player.angle / 180 * Math.PI))
    }
    else {
      return vec2(player.pos.x - 85 * Math.cos(-0.15 + player.angle / 180 * Math.PI), player.pos.y - 92 * Math.sin(0.20 + player.angle / 180 * Math.PI))
    }
  }
  
  onDraw(() => {
    Terrain.drawTerrain()
  })

  if (newLevel) {
    Terrain.seedTerrain()
    Terrain.interpolateLinear()
  }
  Terrain.tCollision()
  
  let wind = addWind(-3, 6)

  onLoad(() => {
    add([
      pos(WIDTH / 2, 450),
      text("Press \"esc\" to return to menu"),
      lifespan(2, { fade: 0.5 }),
      anchor("center")
    ])
  })
  
  const player_1 = add(player)
  if (newLevel) player_1.pos = vec2(80, HEIGHT - Terrain.points[80] - 27)
  player_1.add([
    sprite("barrel"),
    scale(3, 3),
    pos(0, -60)
  ])
  
  const player_2 = add(player2)
  if (newLevel) player_2.pos = vec2(1500, HEIGHT - Terrain.points[1500] - 27)
  player_2.add([
    sprite("barrel"),
    scale(3, 3),
    pos(0, -65),
    rotate(180),
    anchor("botleft")
  ])

  let currentPlayer = currentTurn == 1 ? player_1 : player_2
  currentPlayer.power = 0
  currentPlayer.angle = 0
  
  onCollide("bullet", "ground", () => {
    destroyAll("bullet")
    go("multiplayer", currentPlayer === player_1 ? 2 : 1, score, false)
  })

  player_1.onCollide("bullet", () => {
    addKaboom(player_1.pos)
    destroy(player_1)
    destroyAll("bullet")
    wait(1.1, () => {go("multiplayer", 1, [score[0], score[1]+1])})
  }) 

  player_2.onCollide("bullet", () => {
    addKaboom(player_2.pos)
    destroy(player_2)
    destroyAll("bullet")
    wait(1.1, () => {go("multiplayer", 2, [score[0]+1, score[1]])})
  })
  
  const angle = add([            //UI for displaying the angle of player
    text(Math.floor(currentPlayer.angle).toString()),
    pos(24, 24)
  ])

  const power = add([          //UI to display power of the shot
    text(Math.floor(currentPlayer.power).toString()),
    pos(100, 24)
  ])

  const scoreCount = add([
    text(score[0] + " : " + score[1]),
    pos(WIDTH / 2, 24)
  ])
  
  onUpdate(() => {
    angle.text = Math.abs(Math.floor(currentPlayer.angle)).toString()  //Update player angle UI
    power.text = Math.floor(currentPlayer.power).toString()

    if (get("bullet").length > 0) {
      if (bullet.pos.x > WIDTH || bullet.pos.x < 0) {
        destroy(bullet)
        go("multiplayer", currentPlayer === player_1 ? 2 : 1, score, false)
      }
      if (bullet.pos.y < 300) {
        bullet.pos.x += wind
      }
    }
  })
  
  let rotationSpeed = player.SPEED_HIGH

  onKeyDown("alt", () => {
    rotationSpeed = player.SPEED_LOW  //Slow rotation when holding alt
  })

  onKeyRelease("alt", () => {
    rotationSpeed = player.SPEED_HIGH  //Normal when alt released
  })
  
  onKeyDown("left", () => {
    if (currentPlayer.angle > currentPlayer.ROTATE_MIN) {
      currentPlayer.angle -= rotationSpeed * dt() //Rotate left
    }
    else {
      currentPlayer.angle = currentPlayer.ROTATE_MIN  //Stop at 70 degrees anticlockwise
    }
  })

  onKeyDown("right", () => {
    if (currentPlayer.angle < currentPlayer.ROTATE_MAX) {
      currentPlayer.angle += rotationSpeed * dt() //Rotate right
    }
    else {
      currentPlayer.angle = currentPlayer.ROTATE_MAX  //Prevent tank rotating the wrong direction
    }
  })

  let smooth = 3
  
  onKeyDown("up", () => {
    smooth += 1.2 * dt()
    if (currentPlayer.power < 100) {
      currentPlayer.power += 0.7 * smooth ** 2 * dt() //Increase power smoothly
    }
    else {
      currentPlayer.power = 100  //Maximum power 100
    }
  })

  onKeyRelease("up", () => {
    smooth = 3
  })

  onKeyDown("down", () => {
    smooth += 1.2 * dt()
    if (currentPlayer.power > 1) {
      currentPlayer.power -= 0.7 * smooth ** 2 * dt()  //Decrease power smoothly
    }
    else {
      currentPlayer.power = 0  //Minimum power 0
    }
  })

  onKeyRelease("down", () => {
    smooth = 3
  })

  let bullet
  onKeyDown("space", () => {
    if (get("bullet").length == 0) {
      bullet = newBullet(getEndOfBarrel(currentPlayer), currentPlayer.angle, currentPlayer === player_1 ? currentPlayer.power : (currentPlayer.power * -1 - 40))
    }
  })

  onKeyDown("a", () => {
    if (currentPlayer.pos.x > currentPlayer.POS_MIN) {
      currentPlayer.pos.x--
      currentPlayer.pos.y = HEIGHT - Terrain.points[currentPlayer.pos.x] - 27 
    }
  })
  
  onKeyDown("d", () => {
    if (currentPlayer.pos.x < currentPlayer.POS_MAX) {
      currentPlayer.pos.x++
      currentPlayer.pos.y = HEIGHT - Terrain.points[currentPlayer.pos.x] - 27
    }
  })
  
  onKeyDown("escape", () => {
    go("main-menu")
  })
  
})

scene("multiplayer-menu", () => {
  addBtn("Join Game", vec2(WIDTH / 2, HEIGHT / 4), () => { })
  addBtn("Create Game", vec2(WIDTH / 2, 450), () => { go("multiplayer") })
  addBtn("Return", vec2(WIDTH / 2, 630), () => { go("main-menu") })
})

scene("main-menu", () => {
  addBtn("Practice", vec2(WIDTH / 2, HEIGHT / 4), () => {go("practice")})
  addBtn("Multiplayer", vec2(WIDTH / 2, 450), () => {go("multiplayer")})
})

go("main-menu")

Challenges

The challenges I encountered in this cycle were making the functions work for both players and finding the position to spawn a bullet based on who's turn it was. A lot of ternary statements were used and new bounds for both players were added to the objects. In order to get the position of the end of the barrel for both players based on their turn a new function was added to the code.

Testing

Tests

Test
Instructions
What I expect
What actually happens
Pass/Fail

1

Run code

Kaboom should run

Kaboom ran normally

Pass

2

Enter "multiplayer"

The multiplayer mode of the game should work as described

Multiplayer mode works

Pass

Evidence

Multiplayer game showing the scoreboard above
4MB
Open
Video evidence of playing multiplayer

Last updated