ゲーム制作記 ~マップを表示して、実際にキャラを動かしてみよう~

2021-05-18

概要

前回はキャラクターを配置し、キャンバス内を移動できるようにしていきました。
https://natural-tearoom.com/game-character-moving/

今回はマップを表示させ、壁の衝突判定まで実装します。

マップを表示

配列で何もないタイル(0)と岩のタイル(1)で表現していきます。
今回使用するマップ画像は以下。64x32でマスの大きさに合わせています。

map

// とりあえずcanvasを作る
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = 640
canvas.height = 640
document.body.append(canvas)

// マップの画像をセットし、0, 1で表現
let mapChip = new Image()
mapChip.src = 'img/map.png'
const map = [
  [0, 0, 1, 0, 1, 0, 0, 0 ,0 ,1 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,1 ,0],
  [0, 1, 0, 0, 0, 1, 1, 1 ,0 ,1 ,0 ,1 ,1 ,0 ,1 ,1 ,1 ,0 ,1 ,0],
  [0, 0, 1, 1, 0, 0, 0, 1 ,0 ,0 ,0 ,1 ,0 ,0 ,0 ,1 ,0 ,0 ,0 ,0],
  [1, 0, 1, 0, 1, 1, 0, 0 ,0 ,1 ,1 ,1 ,1 ,1 ,0 ,0 ,1 ,0 ,1 ,0],
  [0, 0, 0, 0, 0, 1, 1, 1 ,0 ,1 ,0 ,0 ,0 ,0 ,1 ,1 ,0 ,1 ,1 ,0],
  [0, 1, 1, 1, 0, 0, 0, 0 ,0 ,1 ,0 ,1 ,1 ,1 ,0 ,1 ,0 ,0 ,0 ,0],
  [0, 1, 1, 1, 0, 1, 1, 1 ,1 ,1 ,0 ,1 ,0 ,0 ,0 ,0 ,1 ,1 ,1 ,0],
  [0, 0, 0, 1, 0, 0, 0, 0 ,1 ,0 ,0 ,1 ,0 ,1 ,1 ,0 ,0 ,0 ,1 ,0],
  [1, 1, 0, 1, 1, 1, 1, 1 ,1 ,0 ,1 ,1 ,0 ,0 ,1 ,1 ,1 ,0 ,1 ,1],
  [1, 0, 0, 0, 0, 0, 1, 1 ,0 ,0 ,0 ,0 ,1 ,0 ,1 ,1 ,0 ,0 ,1 ,0],
  [1, 0, 1, 1, 1, 0, 0, 0 ,1 ,0 ,1 ,0 ,0 ,0 ,0 ,0 ,1 ,1 ,0 ,0],
  [1, 0, 1, 0, 1, 1, 1, 0 ,1 ,0 ,1 ,1 ,0 ,1 ,1 ,0 ,0 ,0 ,0 ,1],
  [0, 0, 1, 0, 0, 1, 0, 0 ,1 ,0 ,0 ,1 ,0 ,1 ,0 ,1 ,1 ,1 ,0 ,0],
  [0, 1, 1, 1, 0, 1, 0, 1 ,0 ,0 ,1 ,1 ,0 ,1 ,0 ,1 ,1 ,0 ,1 ,0],
  [0, 0, 0, 1, 0, 1, 0, 0 ,1 ,0 ,1 ,1 ,0 ,1 ,0 ,0 ,0 ,0 ,0 ,0],
  [1, 1, 0, 1, 0, 1, 0, 1 ,1 ,0 ,0 ,1 ,0 ,1 ,1 ,0 ,1 ,1 ,1 ,0],
  [0, 0, 0, 1, 0, 1, 1, 1 ,1 ,1 ,0 ,1 ,0 ,1 ,1 ,0 ,0 ,0 ,1 ,0],
  [0, 1, 1, 1, 0, 1, 0, 0 ,0 ,0 ,0 ,1 ,0 ,0 ,0 ,1 ,1 ,0 ,1 ,1],
  [0, 1, 0, 0, 0, 1, 0, 1 ,1 ,1 ,0 ,0 ,1 ,1 ,0 ,1 ,0 ,0 ,0 ,0],
  [0, 0, 0, 1, 0, 0, 0, 1 ,1 ,1 ,1 ,0 ,0 ,0 ,1 ,1 ,1 ,1 ,1 ,0]
]
// 配列を回して、mapChipから描写するタイルを選択し、描画する
// drawImageによって画像のどこからどこまでを切り取り、それをどこからどこまで描写させるのかを指定できる
function createMap() {
  for (let y = 0; y < map.length; y++) {
    for (let x = 0; x <= map[y].length; x++) {
      // tile
      if ( map[y][x] === 0 ) ctx.drawImage( mapChip, 0, 0, 32, 32, 32*x, 32*y, 32, 32 );
      // rock
      if ( map[y][x] === 1 ) ctx.drawImage( mapChip, 32, 0, 32, 32, 32*x, 32*y, 32, 32 );
    }
  }
}

addEventListener('load', createMap)

これで配置が完了しました。

map done

衝突判定

ここで岩に当たると先に進めなくなるのはみてわかると思います。
どうやってこれを表現するのでしょうか。

キャラクターの位置は、0と1で表現したマップで特定します。

const map = [
  [0, 0, 1, 0, 1, 0, 0, 0 ,0 ,1 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,1 ,0],
  [0, 1, 0, 0, 0, 1, 1, 1 ,0 ,1 ,0 ,1 ,1 ,0 ,1 ,1 ,1 ,0 ,1 ,0],
  [0, 0, 1, 1, 0, 0, 0, 1 ,0 ,0 ,0 ,1 ,0 ,0 ,0 ,1 ,0 ,0 ,0 ,0],
  [1, 0, 1, 0, 1, 1, 0, 0 ,0 ,1 ,1 ,1 ,1 ,1 ,0 ,0 ,1 ,0 ,1 ,0],
  [0, 0, 0, 0, 0, 1, 1, 1 ,0 ,1 ,0 ,0 ,0 ,0 ,1 ,1 ,0 ,1 ,1 ,0],
  [0, 1, 1, 1, 0, 0, 0, 0 ,0 ,1 ,0 ,1 ,1 ,1 ,0 ,1 ,0 ,0 ,0 ,0],
  [0, 1, 1, 1, 0, 1, 1, 1 ,1 ,1 ,0 ,1 ,0 ,0 ,0 ,0 ,1 ,1 ,1 ,0],
  [0, 0, 0, 1, 0, 0, 0, 0 ,1 ,0 ,0 ,1 ,0 ,1 ,1 ,0 ,0 ,0 ,1 ,0],
  [1, 1, 0, 1, 1, 1, 1, 1 ,1 ,0 ,1 ,1 ,0 ,0 ,1 ,1 ,1 ,0 ,1 ,1],
  [1, 0, 0, 0, 0, 0, 1, 1 ,0 ,0 ,0 ,0 ,1 ,0 ,1 ,1 ,0 ,0 ,1 ,0],
  [1, 0, 1, 1, 1, 0, 0, 0 ,1 ,0 ,1 ,0 ,0 ,0 ,0 ,0 ,1 ,1 ,0 ,0],
  [1, 0, 1, 0, 1, 1, 1, 0 ,1 ,0 ,1 ,1 ,0 ,1 ,1 ,0 ,0 ,0 ,0 ,1],
  [0, 0, 1, 0, 0, 1, 0, 0 ,1 ,0 ,0 ,1 ,0 ,1 ,0 ,1 ,1 ,1 ,0 ,0],
  [0, 1, 1, 1, 0, 1, 0, 1 ,0 ,0 ,1 ,1 ,0 ,1 ,0 ,1 ,1 ,0 ,1 ,0],
  [0, 0, 0, 1, 0, 1, 0, 0 ,1 ,0 ,1 ,1 ,0 ,1 ,0 ,0 ,0 ,0 ,0 ,0],
  [1, 1, 0, 1, 0, 1, 0, 1 ,1 ,0 ,0 ,1 ,0 ,1 ,1 ,0 ,1 ,1 ,1 ,0],
  [0, 0, 0, 1, 0, 1, 1, 1 ,1 ,1 ,0 ,1 ,0 ,1 ,1 ,0 ,0 ,0 ,1 ,0],
  [0, 1, 1, 1, 0, 1, 0, 0 ,0 ,0 ,0 ,1 ,0 ,0 ,0 ,1 ,1 ,0 ,1 ,1],
  [0, 1, 0, 0, 0, 1, 0, 1 ,1 ,1 ,0 ,0 ,1 ,1 ,0 ,1 ,0 ,0 ,0 ,0],
  [0, 0, 0, 1, 0, 0, 0, 1 ,1 ,1 ,1 ,0 ,0 ,0 ,1 ,1 ,1 ,1 ,1 ,0]
]

ここで重要なことはキャラクターのx,y進んだ数を32で割るとmap配列のどこにいるかわかるということです。

例えば、iwtキャラクターが初期位置から右に64移動したとします。 すると、以下の計算が成り立ちます。

x = 64 / 32 = 2
// yは0のままなので、位置は以下のように特定できる
map[y = 0][x = 2]  // ここはマップでは1に当たるため岩のタイルとなる

yも然りです。

前回のコードに付け足して、条件分岐を加えていきます。

if ( iwt.move === 0 ) {
    let x = iwt.x / 32
    let y = iwt.y / 32
    if (key.left) {
      // leftだと-1
      x--
      if (map[y][x] === 0) {
        iwt.move = 32
        key.push = 'left'
      }
    }
    if (key.up) {
      // upだと-1
      y--
      // y値が負だとundefinedなので0以上であることを保証する
      if (y >= 0 && map[y][x] === 0) {
        iwt.move = 32
        key.push = 'up'
      }
    }
    if (key.right) {
      // 右だと+1
      x++
      if (map[y][x] === 0) {
        iwt.move = 32
        key.push = 'right'
      }
    }
    if (key.down) {
      // yだと+1
      y++
      // yが自由に動けるのは配列上で0-19
      if (y < 20 && map[y][x] === 0) {
        iwt.move = 32
        key.push = 'down'
      }
    }
}

全体のコード

前回のと合わせたコードを載せます。

const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = 640
canvas.height = 640
document.body.append(canvas)

let iwt = {
  img: new Image(),
  x: 0,
  y: 0,
  move: 0
}
let key = {
  up: false,
  down: false,
  left: false,
  right: false,
  push: ''
}
iwt.img.src = 'img/iwt.png'

let mapChip = new Image()
mapChip.src = 'img/map.png'
const map = [
  [0, 0, 1, 0, 1, 0, 0, 0 ,0 ,1 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,1 ,0],
  [0, 1, 0, 0, 0, 1, 1, 1 ,0 ,1 ,0 ,1 ,1 ,0 ,1 ,1 ,1 ,0 ,1 ,0],
  [0, 0, 1, 1, 0, 0, 0, 1 ,0 ,0 ,0 ,1 ,0 ,0 ,0 ,1 ,0 ,0 ,0 ,0],
  [1, 0, 1, 0, 1, 1, 0, 0 ,0 ,1 ,1 ,1 ,1 ,1 ,0 ,0 ,1 ,0 ,1 ,0],
  [0, 0, 0, 0, 0, 1, 1, 1 ,0 ,1 ,0 ,0 ,0 ,0 ,1 ,1 ,0 ,1 ,1 ,0],
  [0, 1, 1, 1, 0, 0, 0, 0 ,0 ,1 ,0 ,1 ,1 ,1 ,0 ,1 ,0 ,0 ,0 ,0],
  [0, 1, 1, 1, 0, 1, 1, 1 ,1 ,1 ,0 ,1 ,0 ,0 ,0 ,0 ,1 ,1 ,1 ,0],
  [0, 0, 0, 1, 0, 0, 0, 0 ,1 ,0 ,0 ,1 ,0 ,1 ,1 ,0 ,0 ,0 ,1 ,0],
  [1, 1, 0, 1, 1, 1, 1, 1 ,1 ,0 ,1 ,1 ,0 ,0 ,1 ,1 ,1 ,0 ,1 ,1],
  [1, 0, 0, 0, 0, 0, 1, 1 ,0 ,0 ,0 ,0 ,1 ,0 ,1 ,1 ,0 ,0 ,1 ,0],
  [1, 0, 1, 1, 1, 0, 0, 0 ,1 ,0 ,1 ,0 ,0 ,0 ,0 ,0 ,1 ,1 ,0 ,0],
  [1, 0, 1, 0, 1, 1, 1, 0 ,1 ,0 ,1 ,1 ,0 ,1 ,1 ,0 ,0 ,0 ,0 ,1],
  [0, 0, 1, 0, 0, 1, 0, 0 ,1 ,0 ,0 ,1 ,0 ,1 ,0 ,1 ,1 ,1 ,0 ,0],
  [0, 1, 1, 1, 0, 1, 0, 1 ,0 ,0 ,1 ,1 ,0 ,1 ,0 ,1 ,1 ,0 ,1 ,0],
  [0, 0, 0, 1, 0, 1, 0, 0 ,1 ,0 ,1 ,1 ,0 ,1 ,0 ,0 ,0 ,0 ,0 ,0],
  [1, 1, 0, 1, 0, 1, 0, 1 ,1 ,0 ,0 ,1 ,0 ,1 ,1 ,0 ,1 ,1 ,1 ,0],
  [0, 0, 0, 1, 0, 1, 1, 1 ,1 ,1 ,0 ,1 ,0 ,1 ,1 ,0 ,0 ,0 ,1 ,0],
  [0, 1, 1, 1, 0, 1, 0, 0 ,0 ,0 ,0 ,1 ,0 ,0 ,0 ,1 ,1 ,0 ,1 ,1],
  [0, 1, 0, 0, 0, 1, 0, 1 ,1 ,1 ,0 ,0 ,1 ,1 ,0 ,1 ,0 ,0 ,0 ,0],
  [0, 0, 0, 1, 0, 0, 0, 1 ,1 ,1 ,1 ,0 ,0 ,0 ,1 ,1 ,1 ,1 ,1 ,0]
]

function createMap() {
  for (let y = 0; y < map.length; y++) {
    for (let x = 0; x <= map[y].length; x++) {
      // tile
      if ( map[y][x] === 0 ) ctx.drawImage( mapChip, 0, 0, 32, 32, 32*x, 32*y, 32, 32 );
      // rock
      if ( map[y][x] === 1 ) ctx.drawImage( mapChip, 32, 0, 32, 32, 32*x, 32*y, 32, 32 );
    }
  }
}

function main() {
  // map制作
  createMap()

  // iwtを表示
  ctx.drawImage(iwt.img, iwt.x, iwt.y)

  // 壁判定
  if ( iwt.move === 0 ) {
    let x = iwt.x / 32
    let y = iwt.y / 32
    if (key.left) {
      x--
      if (map[y][x] === 0) {
        iwt.move = 32
        key.push = 'left'
      }
    }
    if (key.up) {
      y--
      if (y >= 0 && map[y][x] === 0) {
        iwt.move = 32
        key.push = 'up'
      }
    }
    if (key.right) {
      x++
      if (map[y][x] === 0) {
        iwt.move = 32
        key.push = 'right'
      }
    }
    if (key.down) {
      y++
      if (y < 20 && map[y][x] === 0) {
        iwt.move = 32
        key.push = 'down'
      }
    }
  }
  if (iwt.move > 0) {
    iwt.move -= 4
    if ( key.push === 'left' ) iwt.x -= 4
    if ( key.push === 'up' ) iwt.y -= 4
    if ( key.push === 'right' ) iwt.x += 4
    if ( key.push === 'down' ) iwt.y += 4
  }
  requestAnimationFrame(main)
}

function handleKeyDown( event ) {
  event.preventDefault();

  const key_code = event.keyCode;
  if( key_code === 37 ) key.left = true;
  if( key_code === 38 ) key.up = true;
  if( key_code === 39 ) key.right = true;
  if( key_code === 40 ) key.down = true;
}
function handleKeyUp( event ) {
  const key_code = event.keyCode;
  if( key_code === 37 ) key.left = false;
  if( key_code === 38 ) key.up = false;
  if( key_code === 39 ) key.right = false;
  if( key_code === 40 ) key.down = false;
}
addEventListener('keydown', handleKeyDown, false)
addEventListener('keyup', handleKeyUp, false)

main()

これで以下のように壁衝突で止まり、canvas外にも出なくなりました。

参考

https://original-game.com/introduction-to-javascript-character-move-on-map/

運営について

Natural Tearoomはシステム開発会社フロントエンドエンジニアがんちゃんが運営するメディアです。
フロントエンド技術を中心に発信しています。

· プライバシーポリシー

SNS

© 2021 天然珈琲店