これは過去に1000回以上解決されていることだと思います。960560のキャンバスと50003000の部屋があり、プレイヤーがどこにいるかによって、常に960*560だけが描画される必要があります。プレイヤーは常に中央にいるべきですが、境界線に近い場合は、最適なビューが計算されるはずです)。プレイヤーはWASDや矢印キーで自由に移動できる。そして、すべてのオブジェクトは自分自身で動くべきです。その代わりに、私はプレーヤー以外のすべてを動かして、プレーヤーが動いているような錯覚を起こさせています。
今、その2つのクエッションが見つかりました。
https://stackoverflow.com/questions/11464550/html5-creating-a-viewport-for-canvas は動作しますが、このタイプのゲームに限って言えば、私のコードでは再現できません。
https://stackoverflow.com/questions/8592872/changing-the-view-center-of-an-html5-canvas は、より有望で、パフォーマンスも高いようですが、私は、他のすべてのオブジェクトをプレーヤーに対して正しく描画することしか理解しておらず、キャンバスのビューポートをプレーヤーに対してスクロールする方法については理解していません。
私のコード(簡略化 - ゲームロジックは別にあります)。
var canvas = document.getElementById("game");
canvas.tabIndex = 0;
canvas.focus();
var cc = canvas.getContext("2d");
// Define viewports for scrolling inside the canvas
/* Viewport x position */ view_xview = 0;
/* Viewport y position */ view_yview = 0;
/* Viewport width */ view_wview = 960;
/* Viewport height */ view_hview = 560;
/* Sector width */ room_width = 5000;
/* Sector height */ room_height = 3000;
canvas.width = view_wview;
canvas.height = view_hview;
function draw()
{
clear();
requestAnimFrame(draw);
// World's end and viewport
if (player.x < 20) player.x = 20;
if (player.y < 20) player.y = 20;
if (player.x > room_width-20) player.x = room_width-20;
if (player.y > room_height-20) player.y = room_height-20;
if (player.x > view_wview/2) ... ?
if (player.y > view_hview/2) ... ?
}
私がやっている方法は全く間違っているようで、どのように試しているのかさえ分かりません...。何かアイデアはありますか?context.transform-thing についてどう思いますか?
私の説明を理解し、どなたかアイデアをお持ちの方がいらっしゃれば幸いです。よろしくお願いします。
[LIVE DEMO](
) at jsfiddle.net このデモでは、実際のゲームシナリオでのビューポートの使用方法を説明します。矢印キーでプレイヤーを部屋の中に移動させます。大きな部屋は矩形を使ってその場で生成され、その結果が画像に保存されます。 プレイヤーは、境界線に近い場合を除き、常に中央にいることに注意してください(お好みで)。それでは、コードの主要な部分、少なくとも見ただけではわかりにくい部分を説明します。
drawImageメソッドのバリエーションとして、新たに8つのパラメータが追加されました。このメソッドを使用して、ソース画像の一部をスライスしてキャンバスに描画することができます。
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) 最初のパラメータ image は、他の変形例と同様に、画像オブジェクトへの参照か、別の canvas 要素への参照です。他の8つのパラメータについては、下の画像を見るのが一番よいでしょう。最初の4つのパラメータはソース画像上のスライスの位置と大きさを定義します。最後の 4 つのパラメータは、出力先キャンバスでの位置とサイズを定義します。 のようになります。 Font: <https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Canvas_tutorial/Using_images>。 How it works in demo: 部屋を表す大きな画像があり、ビューポート内にある部分のみをキャンバス上に表示したいとします。クロップ位置 (sx, sy) はカメラの位置 (xView, yView) と同じで、クロップ寸法はビューポート (canvas) と同じなので
sWidth=canvas.width
とsHeight=canvas.height
となります。 クロップ位置やクロップ寸法が無効な場合はdrawImage
がキャンバスに何も描画しないため、クロップ寸法に注意する必要があります。そのため、以下のようなif
セクションが必要になります。
var sx, sy, dx, dy;
var sWidth, sHeight, dWidth, dHeight;
// offset point to crop the image
sx = xView;
sy = yView;
// dimensions of cropped image
sWidth = context.canvas.width;
sHeight = context.canvas.height;
// if cropped image is smaller than canvas we need to change the source dimensions
if(image.width - sx < sWidth){
sWidth = image.width - sx;
}
if(image.height - sy < sHeight){
sHeight = image.height - sy;
}
// location on canvas to draw the croped image
dx = 0;
dy = 0;
// match destination with source to not scale the image
dWidth = sWidth;
dHeight = sHeight;
// draw the cropped image
context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
ゲームを書くとき、ゲーム内の各オブジェクトのロジックとレンダリングを分離するのは良い習慣です。そこで、Demoでは update
と draw
という関数を用意しています。updateメソッドはオブジェクトの状態を変更します。例えば、quot;game world" 上の位置、物理の適用、アニメーションの状態などです。draw
メソッドは実際にオブジェクトをレンダリングします。ビューポートを考慮して適切にレンダリングするために、オブジェクトはレンダーコンテキストとビューポートのプロパティを知っている必要があります。
ゲームオブジェクトは、ゲーム世界の位置を考慮して更新されることに注意してください。つまり、オブジェクトの(x,y)位置はワールド内の位置ということになります。しかし、ビューポートは変化するので、オブジェクトは適切にレンダリングされる必要があり、レンダリング位置はワールドの位置と異なることになります。
変換は簡単だ。
World(room)内のオブジェクトの位置: (x, y)
ビューポートの位置: (xView, yView)
.
レンダーの位置。(x-xView、y-yView)`となります。
これは、あらゆる種類の座標に対して、たとえ負の座標であっても機能します。
私たちのゲームオブジェクトは、独立した更新メソッドを持っています。Demoの実装では、カメラもゲームオブジェクトとして扱い、個別の更新メソッドを持っています。
カメラオブジェクトは、ビューポートの左上位置 (xView, yView)
, 追従するオブジェクト, ビューポートを表す矩形, ゲーム世界の境界を表す矩形, カメラが動き出す前にプレイヤーが移動できる各境界の最小距離 (xDeadZone, yDeadZone) を保持しています。また、カメラの自由度(軸)も定義しました。RPGのようなトップビューのゲームでは、カメラはx軸(水平方向)とy軸(垂直方向)の両方に動くことができます。
プレイヤーがビューポートの中央にいるように、各軸のデッドゾーンをキャンバスの中心に収束するように設定します。コードの中の以下の関数を見てください。
camera.follow(player, canvas.width/2, canvas.height/2) 注意:マップ(ルーム)のいずれかの寸法がキャンバスより小さい場合、これは期待される動作を生成しないので、以下のUPDATEセクションを参照してください。
世界の限界
カメラを含む各オブジェクトは独自の更新関数を持っているので、ゲーム世界の境界を確認するのは簡単です。ただし、更新関数の最後には、動きを止めるコードを記述することを忘れないでください。
デモの様子
全コードをご覧いただき、ご自身で試してみてください。コードのほとんどの部分には、ガイドとなるコメントがあります。ここでは、あなたがJavascriptの基礎とプロトタイプの扱い方を知っていると仮定します(Javaなどの言語では、プロトタイプオブジェクトはClassと同様の動作をするため、私は時々この用語を使用します)。 [ライブデモ**](
) フルコードです。
<!DOCTYPE HTML>
<html>
<body>
<canvas id="gameCanvas" width=400 height=400 />
<script>
// wrapper for our game "classes", "methods" and "objects"
window.Game = {};
// wrapper for "class" Rectangle
(function() {
function Rectangle(left, top, width, height) {
this.left = left || 0;
this.top = top || 0;
this.width = width || 0;
this.height = height || 0;
this.right = this.left + this.width;
this.bottom = this.top + this.height;
}
Rectangle.prototype.set = function(left, top, /*optional*/ width, /*optional*/ height) {
this.left = left;
this.top = top;
this.width = width || this.width;
this.height = height || this.height
this.right = (this.left + this.width);
this.bottom = (this.top + this.height);
}
Rectangle.prototype.within = function(r) {
return (r.left <= this.left &&
r.right >= this.right &&
r.top <= this.top &&
r.bottom >= this.bottom);
}
Rectangle.prototype.overlaps = function(r) {
return (this.left < r.right &&
r.left < this.right &&
this.top < r.bottom &&
r.top < this.bottom);
}
// add "class" Rectangle to our Game object
Game.Rectangle = Rectangle;
})();
// wrapper for "class" Camera (avoid global objects)
(function() {
// possibles axis to move the camera
var AXIS = {
NONE: 1,
HORIZONTAL: 2,
VERTICAL: 3,
BOTH: 4
};
// Camera constructor
function Camera(xView, yView, viewportWidth, viewportHeight, worldWidth, worldHeight) {
// position of camera (left-top coordinate)
this.xView = xView || 0;
this.yView = yView || 0;
// distance from followed object to border before camera starts move
this.xDeadZone = 0; // min distance to horizontal borders
this.yDeadZone = 0; // min distance to vertical borders
// viewport dimensions
this.wView = viewportWidth;
this.hView = viewportHeight;
// allow camera to move in vertical and horizontal axis
this.axis = AXIS.BOTH;
// object that should be followed
this.followed = null;
// rectangle that represents the viewport
this.viewportRect = new Game.Rectangle(this.xView, this.yView, this.wView, this.hView);
// rectangle that represents the world's boundary (room's boundary)
this.worldRect = new Game.Rectangle(0, 0, worldWidth, worldHeight);
}
// gameObject needs to have "x" and "y" properties (as world(or room) position)
Camera.prototype.follow = function(gameObject, xDeadZone, yDeadZone) {
this.followed = gameObject;
this.xDeadZone = xDeadZone;
this.yDeadZone = yDeadZone;
}
Camera.prototype.update = function() {
// keep following the player (or other desired object)
if (this.followed != null) {
if (this.axis == AXIS.HORIZONTAL || this.axis == AXIS.BOTH) {
// moves camera on horizontal axis based on followed object position
if (this.followed.x - this.xView + this.xDeadZone > this.wView)
this.xView = this.followed.x - (this.wView - this.xDeadZone);
else if (this.followed.x - this.xDeadZone < this.xView)
this.xView = this.followed.x - this.xDeadZone;
}
if (this.axis == AXIS.VERTICAL || this.axis == AXIS.BOTH) {
// moves camera on vertical axis based on followed object position
if (this.followed.y - this.yView + this.yDeadZone > this.hView)
this.yView = this.followed.y - (this.hView - this.yDeadZone);
else if (this.followed.y - this.yDeadZone < this.yView)
this.yView = this.followed.y - this.yDeadZone;
}
}
// update viewportRect
this.viewportRect.set(this.xView, this.yView);
// don't let camera leaves the world's boundary
if (!this.viewportRect.within(this.worldRect)) {
if (this.viewportRect.left < this.worldRect.left)
this.xView = this.worldRect.left;
if (this.viewportRect.top < this.worldRect.top)
this.yView = this.worldRect.top;
if (this.viewportRect.right > this.worldRect.right)
this.xView = this.worldRect.right - this.wView;
if (this.viewportRect.bottom > this.worldRect.bottom)
this.yView = this.worldRect.bottom - this.hView;
}
}
// add "class" Camera to our Game object
Game.Camera = Camera;
})();
// wrapper for "class" Player
(function() {
function Player(x, y) {
// (x, y) = center of object
// ATTENTION:
// it represents the player position on the world(room), not the canvas position
this.x = x;
this.y = y;
// move speed in pixels per second
this.speed = 200;
// render properties
this.width = 50;
this.height = 50;
}
Player.prototype.update = function(step, worldWidth, worldHeight) {
// parameter step is the time between frames ( in seconds )
// check controls and move the player accordingly
if (Game.controls.left)
this.x -= this.speed * step;
if (Game.controls.up)
this.y -= this.speed * step;
if (Game.controls.right)
this.x += this.speed * step;
if (Game.controls.down)
this.y += this.speed * step;
// don't let player leaves the world's boundary
if (this.x - this.width / 2 < 0) {
this.x = this.width / 2;
}
if (this.y - this.height / 2 < 0) {
this.y = this.height / 2;
}
if (this.x + this.width / 2 > worldWidth) {
this.x = worldWidth - this.width / 2;
}
if (this.y + this.height / 2 > worldHeight) {
this.y = worldHeight - this.height / 2;
}
}
Player.prototype.draw = function(context, xView, yView) {
// draw a simple rectangle shape as our player model
context.save();
context.fillStyle = "black";
// before draw we need to convert player world's position to canvas position
context.fillRect((this.x - this.width / 2) - xView, (this.y - this.height / 2) - yView, this.width, this.height);
context.restore();
}
// add "class" Player to our Game object
Game.Player = Player;
})();
// wrapper for "class" Map
(function() {
function Map(width, height) {
// map dimensions
this.width = width;
this.height = height;
// map texture
this.image = null;
}
// creates a prodedural generated map (you can use an image instead)
Map.prototype.generate = function() {
var ctx = document.createElement("canvas").getContext("2d");
ctx.canvas.width = this.width;
ctx.canvas.height = this.height;
var rows = ~~(this.width / 44) + 1;
var columns = ~~(this.height / 44) + 1;
var color = "red";
ctx.save();
ctx.fillStyle = "red";
for (var x = 0, i = 0; i < rows; x += 44, i++) {
ctx.beginPath();
for (var y = 0, j = 0; j < columns; y += 44, j++) {
ctx.rect(x, y, 40, 40);
}
color = (color == "red" ? "blue" : "red");
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
}
ctx.restore();
// store the generate map as this image texture
this.image = new Image();
this.image.src = ctx.canvas.toDataURL("image/png");
// clear context
ctx = null;
}
// draw the map adjusted to camera
Map.prototype.draw = function(context, xView, yView) {
// easiest way: draw the entire map changing only the destination coordinate in canvas
// canvas will cull the image by itself (no performance gaps -> in hardware accelerated environments, at least)
/*context.drawImage(this.image, 0, 0, this.image.width, this.image.height, -xView, -yView, this.image.width, this.image.height);*/
// didactic way ( "s" is for "source" and "d" is for "destination" in the variable names):
var sx, sy, dx, dy;
var sWidth, sHeight, dWidth, dHeight;
// offset point to crop the image
sx = xView;
sy = yView;
// dimensions of cropped image
sWidth = context.canvas.width;
sHeight = context.canvas.height;
// if cropped image is smaller than canvas we need to change the source dimensions
if (this.image.width - sx < sWidth) {
sWidth = this.image.width - sx;
}
if (this.image.height - sy < sHeight) {
sHeight = this.image.height - sy;
}
// location on canvas to draw the croped image
dx = 0;
dy = 0;
// match destination with source to not scale the image
dWidth = sWidth;
dHeight = sHeight;
context.drawImage(this.image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
}
// add "class" Map to our Game object
Game.Map = Map;
})();
// Game Script
(function() {
// prepaire our game canvas
var canvas = document.getElementById("gameCanvas");
var context = canvas.getContext("2d");
// game settings:
var FPS = 30;
var INTERVAL = 1000 / FPS; // milliseconds
var STEP = INTERVAL / 1000 // seconds
// setup an object that represents the room
var room = {
width: 500,
height: 300,
map: new Game.Map(500, 300)
};
// generate a large image texture for the room
room.map.generate();
// setup player
var player = new Game.Player(50, 50);
// Old camera setup. It not works with maps smaller than canvas. Keeping the code deactivated here as reference.
/* var camera = new Game.Camera(0, 0, canvas.width, canvas.height, room.width, room.height);*/
/* camera.follow(player, canvas.width / 2, canvas.height / 2); */
// Set the right viewport size for the camera
var vWidth = Math.min(room.width, canvas.width);
var vHeight = Math.min(room.height, canvas.height);
// Setup the camera
var camera = new Game.Camera(0, 0, vWidth, vHeight, room.width, room.height);
camera.follow(player, vWidth / 2, vHeight / 2);
// Game update function
var update = function() {
player.update(STEP, room.width, room.height);
camera.update();
}
// Game draw function
var draw = function() {
// clear the entire canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// redraw all objects
room.map.draw(context, camera.xView, camera.yView);
player.draw(context, camera.xView, camera.yView);
}
// Game Loop
var gameLoop = function() {
update();
draw();
}
// <-- configure play/pause capabilities:
// Using setInterval instead of requestAnimationFrame for better cross browser support,
// but it's easy to change to a requestAnimationFrame polyfill.
var runningId = -1;
Game.play = function() {
if (runningId == -1) {
runningId = setInterval(function() {
gameLoop();
}, INTERVAL);
console.log("play");
}
}
Game.togglePause = function() {
if (runningId == -1) {
Game.play();
} else {
clearInterval(runningId);
runningId = -1;
console.log("paused");
}
}
// -->
})();
// <-- configure Game controls:
Game.controls = {
left: false,
up: false,
right: false,
down: false,
};
window.addEventListener("keydown", function(e) {
switch (e.keyCode) {
case 37: // left arrow
Game.controls.left = true;
break;
case 38: // up arrow
Game.controls.up = true;
break;
case 39: // right arrow
Game.controls.right = true;
break;
case 40: // down arrow
Game.controls.down = true;
break;
}
}, false);
window.addEventListener("keyup", function(e) {
switch (e.keyCode) {
case 37: // left arrow
Game.controls.left = false;
break;
case 38: // up arrow
Game.controls.up = false;
break;
case 39: // right arrow
Game.controls.right = false;
break;
case 40: // down arrow
Game.controls.down = false;
break;
case 80: // key P pauses the game
Game.togglePause();
break;
}
}, false);
// -->
// start the game when page is loaded
window.onload = function() {
Game.play();
}
</script>
</body>
</html>
更新情報 UPDATE
マップ(部屋)の幅や高さがキャンバスより小さい場合、以前のコードは正しく動作しません。これを解決するには、ゲームスクリプトで、カメラの設定を以下のようにします。
// Set the right viewport size for the camera
var vWidth = Math.min(room.width, canvas.width);
var vHeight = Math.min(room.height, canvas.height);
var camera = new Game.Camera(0, 0, vWidth, vHeight, room.width, room.height);
camera.follow(player, vWidth / 2, vHeight / 2);
camera.follow
関数も更新する必要があります。間違いがあれば報告するか、提案を追加してください。
**キャンバスを、キャンバスよりも大きな画像上のビューポートとして使用する方法について説明します。
ビューポートとは、実際にはユーザーに表示される大きな画像の一部を切り取ったものです。
この場合、ビューポートはキャンバス上でユーザーに表示されることになります(キャンバスがビューポートです)。
まず、ビューポートを大きな画像の周りに移動させるmove関数をコーディングします。
この関数は、ビューポートの上/左隅を指定された方向に5px移動させます。
function move(direction){
switch (direction){
case "left":
left-=5;
break;
case "up":
top-=5;
break;
case "right":
left+=5;
break;
case "down":
top+=5
break;
}
draw(top,left);
}
move関数はdraw関数を呼び出します。
draw()では、drawImage
関数が大きな画像の指定された部分を切り出します。
また、drawImage
は、その「切り取られた背景」をキャンバス上にユーザに対して表示します。
context.clearRect(0,0,game.width,game.height);
context.drawImage(background,cropLeft,cropTop,cropWidth,cropHeight,
0,0,viewWidth,viewHeight);
この例では
Backgroundは全背景画像(通常は表示されず、むしろトリミングのためのソースとなる)。
cropLeft & cropTop は、背景画像のどこから切り抜くかを定義します。
cropWidth & cropHeight は、背景画像から切り取られる矩形の大きさを定義します。
0,0は、背景から切り取られたサブイメージがビューポートキャンバス上の0,0に描かれることを意味します。
viewWidth & viewHeightは、ビューポートキャンバスの幅と高さです。
では、数字を使ったdrawImageの例です。
例えば、ビューポート(=表示用キャンバス)が幅150ピクセル、高さ100ピクセルだとします。
context.drawImage(background,75,50,150,100,0,0,150,100);
75 & 50は、背景画像のx=75/y=50の位置からトリミングを開始することを意味します。
150,100は、切り取る矩形が幅150、高さ100であることを意味します。
0,0,150,100は、切り取られた矩形画像がビューポートキャンバスのフルサイズを使用して表示されることを意味します。
ビューポートの描画の仕組みは以上です。キーコントロールを追加するだけです。
以下はコードとFiddleです:
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
body{ background-color: ivory; }
canvas{border:1px solid red;}
</style>
<script>
$(function(){
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var game=document.getElementById("game");
var gameCtx=game.getContext("2d");
var left=20;
var top=20;
var background=new Image();
background.onload=function(){
canvas.width=background.width/2;
canvas.height=background.height/2;
gameCtx.fillStyle="red";
gameCtx.strokeStyle="blue";
gameCtx.lineWidth=3;
ctx.fillStyle="red";
ctx.strokeStyle="blue";
ctx.lineWidth=3;
move(top,left);
}
background.src="https://dl.dropboxusercontent.com/u/139992952/stackoverflow/game.jpg";
function move(direction){
switch (direction){
case "left":
left-=5;
break;
case "up":
top-=5;
break;
case "right":
left+=5;
break;
case "down":
top+=5
break;
}
draw(top,left);
}
function draw(top,left){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(background,0,0,background.width,background.height,0,0,canvas.width,canvas.height);
gameCtx.clearRect(0,0,game.width,game.height);
gameCtx.drawImage(background,left,top,250,150,0,0,250,150);
gameCtx.beginPath();
gameCtx.arc(125,75,10,0,Math.PI*2,false);
gameCtx.closePath();
gameCtx.fill();
gameCtx.stroke();
ctx.beginPath();
ctx.rect(left/2,top/2,125,75);
ctx.stroke();
ctx.beginPath();
ctx.arc(left/2+125/2,top/2+75/2,5,0,Math.PI*2,false);
ctx.stroke();
ctx.fill();
}
$("#moveLeft").click(function(){move("left");});
$("#moveRight").click(function(){move("right");});
$("#moveUp").click(function(){move("up");});
$("#moveDown").click(function(){move("down");});
}); // end $(function(){});
</script>
</head>
<body>
<canvas id="game" width=250 height=150></canvas><br>
<canvas id="canvas" width=500 height=300></canvas><br>
<button id="moveLeft">Left</button>
<button id="moveRight">Right</button>
<button id="moveUp">Up</button>
<button id="moveDown">Down</button>
</body>
</html>
今のやり方は正しいように思います。そうすれば、レベルやゲーム全体の境界を簡単に変更することができます。
このロジックを抽象化して、特定の "Viewport" メソッドにすると、マップ上の "Camera" が必要な場所を決定し、キャラクターの X および Y 座標がカメラの中心に一致するように必要な計算を処理するだけでよくなります。
また、その方法を反転させて、キャラクターの位置を元にカメラの位置を決めることもできます(例.(position.x - (desired_camera_size.width / 2))`) そして、そこからカメラを描画します。
カメラの位置が決まったら、キャンバスの最初のレイヤーとして、部屋そのものを描くことに専念してください。