Large Display Size Middle Display Size Small Display Size
印刷用 概要 キーワード 著者

スムーズ回転迷路のプログラム

余計な「イベント起動」や「画面の色付け」、「キー入力の工夫」部分などを削除して、

移動・回転アニメのみにしぼったプログラムです。

内容を最小限にしぼったので、参考にしやすいと思います。


▲クリックするとJavaScriptを実行します。(タッチ操作可)

キー入力の工夫を削除したので、操作性は悪くなっていますが、キー入力の工夫についてはこのページの後に記事を作りましたので参考にしてください。

プログラム

ひとつのファイルにまとめ、余計なプログラムを削除したものの、やや長いプログラムになってしまいました。

このような長いプログラムを示されても、理解するのは簡単ではないと思います。

とりあえず、ざっと眺めてもらえばいいかなと思います。

関数は全部で15個ありますが、コメントを使って、用途別に区切ってあります。

例: //--lib common. ~いくつかの関数~ //--/lib common.

用途別区切り 関数リスト 概要
lib common ~ /lib common $( id )
kaitenFunction()
汎用的な関数。
lib 3D ~ /3D makeLinkFrom( masterID ) 3Dに関するデータや関数。
makeLinkFrom()は、3DCGソフトでよく使われる「リンク形状」を実現する関数。
lib map ~ /map mapcheck( mx, my ) 2Dマップデータと、
mx, myの位置にあるマップデータを得る関数。
メイン ~ /メイン onloadx()
run()
draw()
プログラムの根幹部分。
onloadx()で始まり、setIntervalでrun()が50msごとに呼ばれ、必要に応じてdraw()で描画されるという基本。
key. ~ /key. keyExec( key )
rotate( dir, key )
walk( dir, key )
キー入力があったら呼ばれる関数をまとめた部分。
rotate()で90度回転のアニメーション、walk()で一歩移動のアニメーションが作成される。
draw. ~ /draw. build3D()
calc3D()
clearDraw()
draw3D()
draw2D()
もとはdraw()1つだった描画処理を、長すぎるので5つに分割したもの。



<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type">
<title>3D Dungeon</title>
<script>

//--lib common.

function $( id ) {
	
return document.getElementById( id );
	
}


//centerX,Yを中心にして、座標X,Yを、角度toThetaだけ回転する。
function kaitenFunction( centerX, centerY, X, Y, toTheta ) {
X -= centerX;
Y -= centerY;

var fromTheta = Math.atan2( Y, X );
var hankei = Math.sqrt( X * X + Y * Y );
var kaitenX = Math.cos( fromTheta + toTheta ) * hankei;
var kaitenY = Math.sin( fromTheta + toTheta ) * hankei;

kaitenX += centerX;
kaitenY += centerY;

return { X : kaitenX, Y : kaitenY };
}

//--/lib common.
//--lib 3D

var objectsMaster = new Object();
var wallHeightRate = .9;
	
//正方形の壁の高さを変更する 初期値.9 1だと正方形。(割合)

//正六面体

var object = new Object();
object.type = "normal";

var tens = new Array();
tens[ 0 ] = { x : -1,
	
y : -1,
	
z : -1
	
};
tens[ 1 ] = { x : 1,
	
y : -1,
	
z : -1
	
};
tens[ 2 ] = { x : 1,
	
y : 1,
	
z : -1
	
};
tens[ 3 ] = { x : -1,
	
y : 1,
	
z : -1
	
};
tens[ 4 ] = { x : -1,
	
y : -1,
	
z : 1
	
};
tens[ 5 ] = { x : 1,
	
y : -1,
	
z : 1
	
};
tens[ 6 ] = { x : 1,
	
y : 1,
	
z : 1
	
};
tens[ 7 ] = { x : -1,
	
y : 1,
	
z : 1
	
};
object.tens = tens;
for( var i = 0; i < tens.length; i++ )
tens[ i ].y *= wallHeightRate;

var mens = new Array();
mens[ 0 ] = [ 3, 2, 1, 0 ];
	
//正面
mens[ 1 ] = [ 2, 6, 5, 1 ];
	
//右面
mens[ 2 ] = [ 6, 7, 4, 5 ];
	
//背面
mens[ 3 ] = [ 7, 3, 0, 4 ];
	
//左面
mens[ 4 ] = [ 7, 6, 2, 3 ];
	
//天面
mens[ 5 ] = [ 1, 5, 4, 0 ];
	
//底面
object.mens = mens;

object.pos = { x : -50, y : 0, z : 300 };
object.scale = 75;
object.kaitenH = 0;
object.kaitenV = 0;
object.id = "cube";

objectsMaster[ object.id ] = object;

//以上 正六面体


function makeLinkFrom( masterID ) {
var master = objectsMaster[ masterID ];

//リンク形状
var object = new Object();
//変えられないもの
object.type = "link";
object.ref
	
= master;

//変えて良いもの
object.id
	
	
= null;
object.pos
	
	
= master.pos;
object.scale
	
= master.scale;
object.kaitenH
	
= master.kaitenH;
object.kaitenV
	
= master.kaitenV;

return object;
}

//--/3D
//--lib map

var map = new Array();
var splitByLetter = function( s ) {
var a = new Array();
for( var i = 0; i < s.length; i++ ) a[ i ] = s.charAt( i );
return a;
};
//0123456789012345
map[ map.length ] = splitByLetter( "1111111111111111" ); //0
map[ map.length ] = splitByLetter( "1010001000001101" ); //1
map[ map.length ] = splitByLetter( "1010101011100001" ); //2
map[ map.length ] = splitByLetter( "1010101001001111" ); //3
map[ map.length ] = splitByLetter( "1000011100111001" ); //4
map[ map.length ] = splitByLetter( "1111000010101101" ); //5
map[ map.length ] = splitByLetter( "1000011010000001" ); //6
map[ map.length ] = splitByLetter( "1011000010101101" ); //7
map[ map.length ] = splitByLetter( "1101011111110101" ); //8
map[ map.length ] = splitByLetter( "1000010000000101" ); //9
map[ map.length ] = splitByLetter( "1101110111011101" ); //10
map[ map.length ] = splitByLetter( "1000000101010101" ); //11
map[ map.length ] = splitByLetter( "1011110101010101" ); //12
map[ map.length ] = splitByLetter( "1010100001010001" ); //13
map[ map.length ] = splitByLetter( "1000101111000111" ); //14
map[ map.length ] = splitByLetter( "1111111111111111" ); //15


function mapcheck( mx, my ) {
//check.
if( my < 0 || my > map.length - 1 ) return false;
if( mx < 0 || mx > map[ 0 ].length - 1 ) return false;
return map[ my ][ mx ];
}

//--/map
//--メイン

var canvasWidth, canvasHeight;
var canvasWidthHalf, canvasHeightHalf;

//マップの表示の広さについて

var masu = 13;
//単位はマス。必ず奇数であること
//3Dでは表示する奥行にあたる。2Dでは表示するマップサイズにあたる。
//check.
if( masu % 2 == 0 )
alert( "masuは奇数でなくてはなりません.\n現在値: " + masu );

var masuHalf = Math.floor( masu / 2 );
	
//内部使用値 masuが11なら5


//3D描画用
var alltens;
var allmens;
var objects;
	
//3D画面に表示する3Dオブジェクトでリンク形状のみ。objectsMasterのほうはリンクの元の形状を保管している。

//移動、回転アニメ制御用
var rotateFLG = false;
var walkFLG = false;
var rotateDir, walkDir;
var walkAddX, walkAddY;


function onloadx() {

//プレイヤーについて
playerX = 1;
playerY = 1;
playerAngle = 2;

//カメラについて
kamera = new Object();
kamera.s = 50;
	
	
//焦点距離 初期値50
kamera.zoom = 11;
	
//描画の拡大
kamera.zback = 85;
//そのマス目の中心を中心に回転するのではなく、
//中心からzback後ろに下がって回転することで回転の動きを遠巻きに表示する

kamera.kaitenH = 3.14 / 2 * playerAngle;
kamera.kaitenV = 0;
kamera.pos = { x : 0, y : -5, z : 0 };

//canvasについて
canvasEL = $( "canvasID" );
canvas = canvasEL.getContext( '2d' );
canvasWidth = canvasEL.width;
canvasHeight = canvasEL.height;
canvasWidthHalf = canvasWidth / 2;
canvasHeightHalf = canvasHeight / 2;

needsDrawing = false;

draw();

//interval開始
timerID = setInterval( "run()", 50 );

//キー入力開始
document.onkeydown
	
= function( e ) { keyExec( e.which ); };

}//func onloadx


function run() {

//回転アニメ処理
if( rotateFLG ) {
kamera.kaitenH += .08 * rotateDir;
//check. カメラの回転が満了したかどうか
if( rotateDir == 1 && kamera.kaitenH >= rotateKameraTo ||
rotateDir == -1 && kamera.kaitenH <= rotateKameraTo ) {
playerAngle += rotateAdd;
//check. playerAngleの範囲は0~3で、超えたらループにする
if( playerAngle == -1 ) playerAngle = 3;
if( playerAngle == 4 ) playerAngle = 0;
//値を清書
kamera.kaitenH = 3.14 / 2 * playerAngle;
rotateFLG = false;
}
needsDrawing = true;
}

//移動アニメ処理
else if( walkFLG ) {
kamera.pos.z += 10 * walkDir;
//check. カメラの移動が満了したかどうか
if( walkDir == 1 && kamera.pos.z >= walkKameraTo ||
walkDir == -1 && kamera.pos.z <= walkKameraTo ) {
playerX += walkAddX;
playerY += walkAddY;
//値を清書
kamera.pos.z = 0;
walkFLG = false;
}
needsDrawing = true;
}

//変化があったときのみ描画
if( needsDrawing ) {
draw();
needsDrawing = false;
}

}//func run


function draw() {
this.build3D();
this.calc3D();

this.clearDraw();

this.draw3D();
this.draw2D();
}

//--/メイン
//--key.

//キー入力
function keyExec( key ) {
switch( key ) {
case 37: rotate( 1, key );
	
break;
	
//左キー 左回転
case 39: rotate( -1, key );
	
break;
	
//右キー 右回転
case 38: walk( 1, key );
	
break;
	
//上キー 前進
case 40: walk( -1, key );
	
break;
	
//下キー 後退
default:
//
	
console.log( key );
}
}

//回転アニメ開始
function rotate( dir, key ) {
//check. すでに回転中なら
if( rotateFLG ) return;
//check. 移動中に回転キーが押されたら
if( walkFLG ) {
return;
}
//回転先
rotateAdd = dir;
rotateKameraTo = 3.14 / 2 * ( playerAngle + dir );
rotateDir = dir;
rotateFLG = true;
}

//移動アニメ開始
function walk( dir, key ) {
//check. すでに移動中なら
if( walkFLG ) return;
//check. 回転中に移動キーが押されたら
if( rotateFLG ) {
return;
}
//移動先
walkAddX = ( ( playerAngle == 3 ) - ( playerAngle == 1 ) ) * dir;
walkAddY = ( ( playerAngle == 2 ) - ( playerAngle == 0 ) ) * dir;
//check. 移動先は壁か?
if( mapcheck( playerX + walkAddX, playerY + walkAddY ) == "1" ) return;
walkKameraTo = 120 * dir;
walkDir = dir;
walkFLG = true;
}

//--/key.
//--draw.

//2Dマップを参照し3Dオブジェクトを配置
function build3D() {
this.objects = new Object();

//3Dを配置
for( var y = -masuHalf; y <= masuHalf; y++ ) {
for( var x = -masuHalf; x <= masuHalf; x++ ) {
var mx = this.playerX + x;
var my = this.playerY + y;
//check.
if( mx < 0 ) continue;
if( my < 0 ) continue;
if( mx > map[ 0 ].length - 1 ) continue;
if( my > map.length - 1 ) continue;

var masuSize = 120;
var gx = x * masuSize;
var gy = -y * masuSize;

switch( mapcheck( mx, my ) ) {
case "1":
//wall
//リンク形状
var object = makeLinkFrom( "cube" );
object.pos = { x : gx, y : 0, z: gy };
object.scale = masuSize / 2;
object.id
	
= "cubeLnk" + Object.keys( this.objects ).length;
this.objects[ object.id ] = object;
break;
default:
}//sw
}//x
}//y
}//func build3D


function calc3D() {
//点の座標をすべて計算済みにする
this.alltens = new Object();

for( var objectID in this.objects ) {
var object = this.objects[ objectID ];
var tens;
if( object.type == "normal" ) {
tens = object.tens;
} else if( object.type == "link" ) {
tens = object.ref.tens;
}
var scale = object.scale;
var pos = object.pos;
var kaitenH = object.kaitenH;
var kaitenV = object.kaitenV;

var tensCalc = new Array();

for( var i = 0; i < tens.length; i++ ) {
var x = tens[ i ].x;
var y = tens[ i ].y;
var z = tens[ i ].z;

x = x * scale;
y = y * scale;
z = z * scale;

//個体について
//x-z 回転
var kaitenKekka = kaitenFunction( 0, 0, x, z, kaitenH );
x = kaitenKekka.X;
z = kaitenKekka.Y;
//z-y 回転
var kaitenKekka = kaitenFunction( 0, 0, z, y, kaitenV );
z = kaitenKekka.X;
y = kaitenKekka.Y;
//移動
x += pos.x;
y += pos.y;
z += pos.z;

//カメラについて
//x-z 回転
var kaitenKekka = kaitenFunction( 0, 0, x, z, -this.kamera.kaitenH );
x = kaitenKekka.X;
z = kaitenKekka.Y;
//z-y 回転
var kaitenKekka = kaitenFunction( 0, 0, z, y, -this.kamera.kaitenV );
z = kaitenKekka.X;
y = kaitenKekka.Y;
//移動
x += -this.kamera.pos.x;
y += -this.kamera.pos.y;
z += -this.kamera.pos.z;

z += this.kamera.zback;


//画面座標へ変換
var h = x * ( this.kamera.s / z );
var v = y * ( this.kamera.s / z );

h = h * this.kamera.zoom;
v = v * this.kamera.zoom;

h = h + this.canvasWidthHalf;
v = -v + this.canvasHeightHalf;

tmp = new Object();
tmp.x = x;
tmp.y = y;
tmp.z = z;
tmp.h = h;
tmp.v = v;
tensCalc[ i ] = tmp;
}//i
this.alltens[ objectID ] = tensCalc;
//ex. alltens[ "cube" ][ 0 ].x
}//objectID


//すべての形状のすべての面を1つの配列に入れる
this.allmens = new Array();

forObjectID : for( var objectID in this.objects ) {
var object = this.objects[ objectID ];
var mens;
if( object.type == "normal" ) {
mens = object.mens;
} else if( object.type == "link" ) {
mens = object.ref.mens;
}

forJ : for( var j = 0; j < mens.length; j++ ) {
var men = mens[ j ];


//面の重心を計算(Z座標のみ)
var sumZ = 0;
for( var i = 0; i < men.length; i++ ) {
var tenIndex = men[ i ];
var z = this.alltens[ objectID ][ tenIndex ].z;
//check. 視界の後ろはスキップ
if( z <= 0 ) continue forJ;

sumZ += z;
}
var jusinZ = sumZ / men.length;


//陰面消去 法線ベクトル法

//1. その面の法線("ベクトルの外積")を求める。
var p1 = this.alltens[ objectID ][ men[ 0 ] ];
var p2 = this.alltens[ objectID ][ men[ 1 ] ];
var p3 = this.alltens[ objectID ][ men[ 2 ] ];
var _housen = new Object();
_housen.x = ( p2.y - p1.y ) * ( p3.z - p2.z )
- ( p2.z - p1.z ) * ( p3.y - p2.y );
_housen.y =
	
( p2.z - p1.z ) * ( p3.x - p2.x )
- ( p2.x - p1.x ) * ( p3.z - p2.z );
_housen.z =
	
( p2.x - p1.x ) * ( p3.y - p2.y )
- ( p2.y - p1.y ) * ( p3.x - p2.x );
//法線は通常、単位ベクトルにするところ、
//ここでは単位ベクトルにする必要はなく、
//省略しているのでその意味で _ を付けた。

//2. その面から視点への線(視線の逆)を求める。
//(面を構成する1つの頂点の座標を逆数にしたものでよい)
var sisenR = new Object();
	
//sisenRのRは"逆"の意味
sisenR.x = -p1.x;
sisenR.y = -p1.y;
sisenR.z = -p1.z;

//3. 法線と視線(の逆)の成す角("ベクトルの内積")を求める。
var kekka =
	
sisenR.x * _housen.x
+ sisenR.y * _housen.y
+ sisenR.z * _housen.z;

//4. その成す角から可視、不可視を判断できる。
//kekka が 0より大 なら成す角は 90°未満 可視(処理続ける)
//kekka が 0以下 なら成す角は 90°以上 不可視(continue)
//(※ちなみにkekka は三角関数のcosθだから上記になる)
if( kekka <= 0 ) continue;


var onemen = new Object();
onemen.men = men;
onemen.owner = object;
onemen.jusinZ = jusinZ;

this.allmens.push( onemen );
}//j

}//objectID


//陰面消去 画家のアルゴリズム
var sortfunc = function( a, b ) {
if( a.jusinZ > b.jusinZ ) return -1;
if( a.jusinZ < b.jusinZ ) return 1;
return 0;
};
this.allmens.sort( sortfunc );

}//func calc3D


function clearDraw() {
this.canvas.fillStyle = "gray";
this.canvas.fillRect( 0, 0, canvasWidth, canvasHeight );
}


function draw3D() {

//すべての面を描画
for( var j = 0; j < this.allmens.length; j++ ) {
var onemen = this.allmens[ j ];
var men = onemen.men;
var object = onemen.owner;
var objectID = object.id;

//ここで面1枚(ポリゴン)を描いている
this.canvas.beginPath();
for( var i = 0; i < men.length; i++ ) {
var tenIndex = men[ i ];

var h = this.alltens[ objectID ][ tenIndex ].h;
var v = this.alltens[ objectID ][ tenIndex ].v;

if( i == 0 )
this.canvas.moveTo( h, v );
else
this.canvas.lineTo( h, v );
}//i
this.canvas.closePath();

//塗りつぶしする
this.canvas.fillStyle = "white";
this.canvas.fill();

//ワイヤフレーム線を描く
this.canvas.strokeStyle = "black";
this.canvas.stroke();
}//j
}//func draw3D


function draw2D() {

var mapZoom = .5;
	
	
	
//2Dマップ表示の拡大 初期値1 (割合)
var alpha = .3;

var masuSZ = 16;
var masuSZHalf = masuSZ / 2;

var width = masuSZ * masu;
var Left = canvasWidth - width * mapZoom;
var Top = canvasHeight - width * mapZoom;

this.canvas.strokeStyle = "RGBA(0,0,0," + alpha + ")";

//壁と床
for( var y = -masuHalf; y < masuHalf + 1; y++ ) {
for( var x = -masuHalf; x < masuHalf + 1; x++ ) {
var mx = this.playerX + x;
var my = this.playerY + y;
//check.
if( mx < 0 ) continue;
if( my < 0 ) continue;
if( mx > map[ 0 ].length - 1 ) continue;
if( my > map.length - 1 ) continue;

var gx = ( masuHalf * masuSZ + x * masuSZ ) * mapZoom;
var gy = ( masuHalf * masuSZ + y * masuSZ ) * mapZoom;

if( mapcheck( mx, my ) == "1" ) {
//wall
this.canvas.fillStyle = "RGBA(128,128,128," + alpha + ")";
this.canvas.fillRect( Left + gx, Top + gy, masuSZ * mapZoom, masuSZ * mapZoom );
this.canvas.strokeRect( Left + gx, Top + gy, masuSZ * mapZoom, masuSZ * mapZoom );
} else {
//way
this.canvas.fillStyle = "RGBA(255,255,255," + alpha + ")";
this.canvas.fillRect( Left + gx, Top + gy, masuSZ * mapZoom, masuSZ * mapZoom );
}
}
}

//player.赤丸
var centerGX = ( masuHalf * masuSZ + masuSZHalf ) * mapZoom;
var centerGY = ( masuHalf * masuSZ + masuSZHalf) * mapZoom;

this.canvas.fillStyle = "RGBA(255,0,0," + alpha + ")";
this.canvas.beginPath();
this.canvas.arc( Left + centerGX, Top + centerGY,
masuSZHalf * mapZoom, 0, 6.29, false );
this.canvas.closePath();
this.canvas.fill();
this.canvas.stroke();


//プレイヤーの見ている方向の目印(オレンジ色の●)
var dirBoxCenterGX = masuHalf * masuSZ + masuSZHalf;
var dirBoxCenterGY = masuHalf * masuSZ + masuSZHalf;
switch( this.playerAngle ) {
case 4:
case 0: dirBoxCenterGY -= masuSZHalf; break;
case 1: dirBoxCenterGX -= masuSZHalf; break;
case 2: dirBoxCenterGY += masuSZHalf; break;
case -1:
case 3: dirBoxCenterGX += masuSZHalf; break;
default:
	

}
dirBoxCenterGX *= mapZoom;
dirBoxCenterGY *= mapZoom;

this.canvas.beginPath();
this.canvas.arc( Left + dirBoxCenterGX, Top + dirBoxCenterGY,
Math.max( 2, masuSZ / 8 * mapZoom ), 0, 6.29, false );
this.canvas.closePath();
this.canvas.fillStyle = "RGBA(255,255,0," + alpha + ")";
this.canvas.fill();
this.canvas.stroke();

}//func draw2D

//--/draw.

</script>
</head>


<body onload="onloadx()">
Hello World! <BR><a href="javascript_withTouch.html">タッチ対応版</a><BR><BR>
<canvas id="canvasID" width="575" height="480" style="
border:solid 1px black;
">
CANVASを使用できません.
</canvas>
</body>
</html>

キー入力の改善

以降はタッチ操作 非対応です。

次の3つのプログラムは順を追って変更していく様子を段階的に並べています。上記のプログラムからの変更が下記の段階1で、段階1の変更が段階2、段階2の変更が段階3です。段階3が現状の一番よいプログラムということになります。

キー入力改善 段階1

キー入力方式をキータイプからキーセンスへ変更

キー入力改善 段階2

次のキーの先行入力を受け付けるように変更

キー入力改善 段階3

回転、移動中に反対方向のキー入力を受け付けるように変更


以上3つの改善でだいぶ操作性が向上すると思います。


ページ制作 homepage6047


ページの上端へ (もくじ開く)