Chapter 38

カメラ

miku
miku
2021.11.15に更新

画面に収まりきれない描画の扱い方の一つに、カメラの概念を導入するという方法がある。つまり描画ではなく画面そのものを動かしてしまえばいい。たとえばマリオのような横スクロールのゲームなどが代表的だ。この章ではそのカメラの実装について扱う。

カメラ

カメラの実装を一つ一つ行っていきたい。描画対象は画像である。まずは簡単のために画像の座標は (0, 0) で固定にする。次にカメラの座標を表す変数 (cam.x, cam.y) を用意する。この座標が描画の基点となり、つまり (cam.x, cam.y) が画面の左上に描画されると考えていい。カメラのサイズだが、これも簡単のため、画面サイズ (width, height) に合わせることにする。まとめると、カメラの概念の導入により、(cam.x, cam.y)~(cam.x + width, cam.y + height) の範囲を画面全体に描画する。とはいっても、p5.jsには範囲の指定で描画を行うような機能は存在しないため、描画対象である画像がカメラの範囲に入っている部分だけを描画するという方法を取る必要がある。

copy(img, cam.x, cam.y, width, height, 0, 0, width, height);

画像の一部分を切り取る必要があるので image() ではなく copy() を利用する。img の座標は (0, 0) と固定しているので、切り抜く左上座標は (cam.x, cam.y) になる。切り抜くサイズはカメラのサイズである (width, height) にすればいい。この範囲を画面にまるまる描画するので、貼り付け先(第6引数以降)は (0, 0, width, height) になる。

let img, cam;

function preload() {
  img = loadImage("0.png");
}

function setup() {
  createCanvas(windowWidth, windowHeight);

  cam = Camera.create(0, 0);
}

function draw() {
  clear();
  copy(img, cam.x, cam.y, width, height, 0, 0, width, height);

  if (mouseIsPressed) {
    if (mouseX < width / 2) {
      cam.x--;
    } else {
      cam.x++;
    }
  }
}

const Camera = {
  create: function (x, y) {
    return { x, y };
  },
};

カメラの動きを確認するために、画面左半分でマウスボタンを押している間は左に、右半分でなら右にカメラを移動させる。

描画対象の座標を考慮する

次に画像の座標を (0, 0) 以外にも配置できるようにしたい。たとえば (1, 0) のように一つ右にずれていたら、画像の切り出す座標が一つ左にずれるということになる。画像の座標を (imgX, imgY) だとすると、切り出し座標は (cam.x, cam.y) から (cam.x - imgX, cam.y - imgY) に変わることになる。

let img, cam, imgX, imgY;

function preload() {
  img = loadImage("0.png");
}

function setup() {
  createCanvas(windowWidth, windowHeight);

  cam = Camera.create(0, 0);

  imgX = 100;
  imgY = 100;
}

function draw() {
  clear();

  const sx = cam.x - imgX;
  const sy = cam.y - imgY;
  const sw = width;
  const sh = height;
  const dx = 0;
  const dy = 0;
  const dw = sw;
  const dh = sh;
  copy(img, sx, sy, sw, sh, dx, dy, dw, dh);

  if (mouseIsPressed) {
    if (mouseX < width / 2) {
      cam.x--;
    } else {
      cam.x++;
    }
  }
}

const Camera = {
  create: function (x, y) {
    return { x, y };
  },
};

カメラのサイズを考慮する

今までカメラのサイズは画面サイズにしていたが、これを任意のサイズに変えれるようにしたい。まず、カメラのオブジェクトにそのサイズを保存できるようにする。あとは画像の切り出しサイズと貼り付けサイズをそのカメラのサイズに変更すればいい。

let img, cam, imgX, imgY;

function preload() {
  img = loadImage("0.png");
}

function setup() {
  createCanvas(windowWidth, windowHeight);

  cam = Camera.create(0, 0, 300, 300);

  imgX = 100;
  imgY = 100;
}

function draw() {
  clear();

  const sx = cam.x - imgX;
  const sy = cam.y - imgY;
  const sw = cam.width;
  const sh = cam.height;
  const dx = 0;
  const dy = 0;
  const dw = sw;
  const dh = sh;
  copy(img, sx, sy, sw, sh, dx, dy, dw, dh);

  push();
  strokeWeight(4);
  stroke(240);
  noFill();
  rect(0, 0, cam.width, cam.height);
  pop();

  if (mouseIsPressed) {
    if (mouseX < width / 2) {
      cam.x--;
    } else {
      cam.x++;
    }
  }
}

const Camera = {
  create: function (x, y, width, height) {
    return { x, y, width, height };
  },
};

画面端を超えないようにする

カメラの導入により、仮に描くものが画面サイズより大きくても、スクロールしていけばすべてを見ることができるようになった。しかし、背景画像などを配置している際、背景をはみだした領域をカメラで映してしまうと見栄えが悪いので、背景画像の領域自体からカメラがはみ出ないようにしたい。ここではその領域のことを便宜上スクリーンと呼ぶ。

let img, cam, imgX, imgY, screenWidth, screenHeight;

function preload() {
  img = loadImage("0.png");
}

function setup() {
  createCanvas(windowWidth, windowHeight);

  cam = Camera.create(0, 0, 300, 300);

  imgX = 100;
  imgY = 100;

  screenWidth = img.width;
  screenHeight = img.height;
}

function draw() {
  clear();

  const sx = cam.x - imgX;
  const sy = cam.y - imgY;
  const sw = cam.width;
  const sh = cam.height;
  const dx = 0;
  const dy = 0;
  const dw = sw;
  const dh = sh;
  copy(img, sx, sy, sw, sh, dx, dy, dw, dh);

  push();
  strokeWeight(4);
  stroke(240);
  noFill();
  rect(0, 0, cam.width, cam.height);
  pop();

  if (mouseIsPressed) {
    if (mouseX < width / 2) {
      cam.x--;
    } else {
      cam.x++;
    }
  }

  if (cam.x < 0) {
    cam.x = 0;
  }
  if (cam.x + cam.width >= screenWidth) {
    cam.x = screenWidth - cam.width - 1;
  }
  if (cam.y < 0) {
    cam.y = 0;
  }
  if (cam.y + cam.height >= screenHeight) {
    cam.y = screenHeight - cam.height - 1;
  }
}

const Camera = {
  create: function (x, y, width, height) {
    return { x, y, width, height };
  },
};

x軸だけで考えると、カメラの左側 (cam.x) が画面左端 (x = 0) より左側に行かなければいいので if (cam.x < 0) と判定すればいい。カメラの右側の判定は (cam.x + cam.width) が画面右端 (x = screenWidth - 1) より大きくなったらだめなので if (cam.x + cam.width >= screenWidth) となる。y軸の計算も同様である。

ミニマップ

カメラがスクリーンの中で、どの地点を描画しているかを把握するにはミニマップを実装すると便利である。

translate(width / 2, 0); // ミニマップを描画する座標
scale(s); // 縮小比率
stroke(240);
noFill();
rect(0, 0, screenWidth, screenHeight);
image(img, imgX, imgY);
stroke(255, 0, 0);
rect(cam.x, cam.y, cam.width, cam.height);
resetMatrix();

scale() で座標を縮小させたあとに、スクリーン領域・カメラ領域・画像を描画する。

let img, cam, imgX, imgY, screenWidth, screenHeight, s;

function preload() {
  img = loadImage("0.png");
}

function setup() {
  createCanvas(windowWidth, windowHeight);

  cam = Camera.create(0, 0, 300, 300);

  imgX = 100;
  imgY = 100;

  screenWidth = img.width;
  screenHeight = img.height;

  s = 0.3;
}

function draw() {
  clear();

  const sx = cam.x - imgX;
  const sy = cam.y - imgY;
  const sw = cam.width;
  const sh = cam.height;
  const dx = 0;
  const dy = 0;
  const dw = sw;
  const dh = sh;
  copy(img, sx, sy, sw, sh, dx, dy, dw, dh);

  push();
  strokeWeight(4);
  stroke(240);
  noFill();
  rect(0, 0, cam.width, cam.height);
  pop();

  if (mouseIsPressed) {
    if (mouseX < width / 2) {
      cam.x--;
    } else {
      cam.x++;
    }
  }

  if (cam.x < 0) {
    cam.x = 0;
  }
  if (cam.x + cam.width >= screenWidth) {
    cam.x = screenWidth - cam.width - 1;
  }
  if (cam.y < 0) {
    cam.y = 0;
  }
  if (cam.y + cam.height >= screenHeight) {
    cam.y = screenHeight - cam.height - 1;
  }

  translate(width / 2, 0);
  scale(s);
  stroke(240);
  noFill();
  rect(0, 0, screenWidth, screenHeight);
  image(img, imgX, imgY);
  stroke(255, 0, 0);
  rect(cam.x, cam.y, cam.width, cam.height);
  resetMatrix();
}

const Camera = {
  create: function (x, y, width, height) {
    return { x, y, width, height };
  },
};

オブジェクトを追う

ゲームなどでは、カメラを自身が操作することは稀で、操作しているキャラクターを常に画面中央あたりに描画するようにカメラが追う形になるのが基本である。

let img, cam, imgX, imgY, screenWidth, screenHeight, s, obj, vx, vy;

function preload() {
  img = loadImage("0.png");
}

function setup() {
  createCanvas(windowWidth, windowHeight);

  cam = Camera.create(0, 0, 300, 300);

  imgX = 100;
  imgY = 100;

  screenWidth = img.width;
  screenHeight = img.height;

  s = 0.3;

  obj = { x: 100, y: 100 };
  vx = 1;
  vy = 1;
  Camera.setTarget(cam, obj);
}

function draw() {
  clear();

  const sx = cam.x - imgX;
  const sy = cam.y - imgY;
  const sw = cam.width;
  const sh = cam.height;
  const dx = 0;
  const dy = 0;
  const dw = sw;
  const dh = sh;
  copy(img, sx, sy, sw, sh, dx, dy, dw, dh);

  push();
  strokeWeight(4);
  stroke(240);
  noFill();
  rect(0, 0, cam.width, cam.height);
  pop();

  if (mouseIsPressed) {
    if (mouseX < width / 2) {
      cam.x--;
    } else {
      cam.x++;
    }
  }

  Camera.update(cam);
  obj.x += vx;
  obj.y += vy;
  if (obj.x < 0) {
    obj.x = 0;
    vx *= -1;
  }
  if (obj.x >= screenWidth) {
    obj.x = screenWidth - 1;
    vx *= -1;
  }
  if (obj.y < 0) {
    obj.y = 0;
    vy *= -1;
  }
  if (obj.y >= screenHeight) {
    obj.y = screenHeight - 1;
    vy *= -1;
  }

  translate(width / 2, 0);
  scale(s);
  stroke(240);
  noFill();
  rect(0, 0, screenWidth, screenHeight);
  image(img, imgX, imgY);
  stroke(255, 0, 0);
  rect(cam.x, cam.y, cam.width, cam.height);
  fill(255, 0, 0);
  circle(obj.x, obj.y, 20);
  resetMatrix();
}

const Camera = {
  create: function (x, y, width, height) {
    return { x, y, width, height };
  },

  setTarget: function (cam, obj) {
    cam.obj = obj;
  },

  update: function (cam) {
    cam.x = obj.x - cam.width / 2;
    cam.y = obj.y - cam.height / 2;

    if (cam.x < 0) {
      cam.x = 0;
    }
    if (cam.x + cam.width >= screenWidth) {
      cam.x = screenWidth - cam.width - 1;
    }
    if (cam.y < 0) {
      cam.y = 0;
    }
    if (cam.y + cam.height >= screenHeight) {
      cam.y = screenHeight - cam.height - 1;
    }
  },
};

Camera.setTarget(cam, obj) でカメラが追うターゲットを指定する。あとは毎フレーム、カメラの領域中央にオブジェクトが来るように位置を調整する。