LoginSignup
0
1

More than 3 years have passed since last update.

javascript 荷物運びゲームを自分なりに解釈 ビット演算子

Last updated at Posted at 2020-07-05

今回も、書籍ゲームで学ぶJavascript入門で紹介されている
運び屋さんゲームの知識定着のために自分なりに解説をしてみます。

ゲーム内容はこちらと同じです。
これを発展させると
switchのファミコンに入ってるアドベンチャーズ オブ ロロのようなゲームになると思います。

ビット処理が出て、よりゲームらしい実装なので
頑張って読み解いていきます。

解説が間違ってたらごめんなさい。

この記事では、これだけわかったらOK

2進法の0と1を活用するビット演算の良い所は、
ある一つの部分を調べたら目的の値だけが絞られることです。
「靴を見れば、おしゃれか分かる」
「2ビット目が0か1で、移動できるか分かる」

2ビット目だけを直接取り出す関数がないので、
意図的に計算して、出た結果から判断をしているだけです。

ビット演算子

荷物が壁に進むなら、動かない。
荷物が道に進むなら、動く。

これはすべてif文で処理ができます。
ここでビット演算を取り入れたら、よりスッキリなコードができます。
ファミコンのマリオブラザーズの容量が40KBしか無いので、簡略にする意味があります。

ビットは、0と1の2進法で表す表記です。
0がOFF、1がONを表します。

AND演算子は、お互いの数字が1のONなら、1のONになります。  1 and 1 = 1

OR演算子は、どちらかの数字が1のONなら、1のONになります。  1 or 0 = 1

また2進法と10進法の表がこちらです。

2進数 10進数
0000   0
0001   1
0010   2
0011   3
0100   4
0101   5
0110   6

10進法の1と2をAND演算子とOR演算子で計算すると

1は  0001
2は  0010

AND演算子は、上下の0と1をそれぞれ比較して
同じ場所で1が重なるところがないので、0000となります。つまり10進法で0です。

0001
0010
:frowning2::frowning2::frowning2::frowning2:
0000 →10進法で0

OR演算子は、上下の0と1をそれぞれ比較して
どちらかが1になる場所は2か所あります。0011となります。つまり10進法で3です。

0001
0010
:frowning2::frowning2::relaxed::relaxed:
0011 →10進法で3

地図の表現

地図となる配列がこちらです。
ここでは40pxの正方形の絵を、積み重ねてマップを構成します。
その数字によって描く絵が変わります。6が壁 0が道 2が荷物 1がゴールです。

map.js
var data = [
    [6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6],
    [6,6,6,6,6,0,0,6,6,6,6,6,6,6,6,6,6,6,6,6],
    [6,6,6,6,6,2,0,0,6,6,6,6,6,6,6,6,6,6,6,6],
    [6,6,6,6,6,0,0,0,0,0,6,6,6,6,6,6,6,6,6,6],
    [6,6,6,0,0,2,0,0,2,0,6,6,6,6,6,6,6,6,6,6],
    [6,6,6,0,6,0,6,6,6,0,6,6,6,6,6,6,6,6,6,6],
    [6,0,0,0,6,0,6,6,6,0,6,6,6,6,6,0,0,1,6,6],
    [6,0,2,0,0,2,0,0,0,0,0,0,0,0,0,0,1,1,6,6],
    [6,6,6,6,6,0,6,6,6,6,0,6,0,6,0,0,1,1,6,6],
    [6,6,6,6,6,0,0,0,0,0,0,6,6,6,6,6,6,6,6,6],
    [6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6],
];

//6が壁 0が道 2が荷物 1がゴール

var gc
var px = 12,py = 8

//gc 絵を描く場所
//pxとpyがキャラクターがいる場所です。


function init(){
 gc = document.getElementByID("soko").getContext("2d");
 onkeydown = mykeydown; // キーを押すと、オリジナル関数mykeydownが発動
 repaint(); //上のdataを元に、マップを描画する
}

そしてhtmlはこうなります。

map.html
<body onload ="init()">
<canvas id="soko" width="800" height="440"></canvas>
<img id ="imgWall" src="imgWall.png" style="display:none" />
<img id ="imgGoal" src="imgGoal.png" style="display:none" />
<img id ="imgWorker" src="imgWorker.png" style="display:none" />
<img id ="imgLuggage" src="imgLuggage.png" style="display:none" />

画像は読み込んでいますが、非表示をします。
repaintの中で、画像を選択して描画するときに使います。

余談ですが下のリンク先の人は、javascriptでマリオブラザーズを作成しています。
javascriptマリオブラザーズ

あらかじめマリオに出てくるアクションを1枚にまとめて、画像の読み込みを1回で終わらせる方法をしています。後は画像の座標を指定すれば、表示されたい画像の一部分だけを表示させています。

物を動かす どこの配列を見るか

矢印キーを押したときに、
矢印の先によって、動作を変える必要があります。
矢印の先が荷物。さらにその先が道なら、荷物が移動させる動作をさせる指示を
矢印の先が荷物。さらにその先が壁なら、荷物が動かない動作をさせる指示をさせます。

map.jpg

現在が真ん中のpx pyとすると
上ボタンをおすと、yの値だけ変わります。
dy0 = py+1
dy1 = py+2
px=dx0=dx1 (xは変わらない)

action.js
function mykeydown(e){
 var dx0=px,dx1=px,dy0=py,dy1=py //ひとまず真ん中の値で、xとyの値にしておく

//どのボタンを押したかで処理を変える 
    switch (e.keyCode){
        case 37: dx0--; dx1 -= 2; //左
        break;
        case 38: dy0--; dy1 -= 2; //下
        break;
        case 39: dx0++; dx1 += 2; // 右
        break;
        case 40: dy0++; dy1 += 2; //上
        break;
    }

/*
左を例にすると
pxが原点
dx0-- は dx0 = dx0-1の略  pxから1つ左にずれた所
dx1-=2 は dx1 = dx1-2の略 pxから2つ左にずれた所

yは変わりません。

*/

テキストにはこのような表記が出てきます。

action.js
if((data[dy0][dx0]&0x2)==0){
}

矢印を押すとキャラクターが動作します。
キャラクターが動いた先が、道やゴールなら、そこにキャラクターを移動できます。
キャラクターが動いた先が、壁や荷物ならキャラクターは移動できないとします。

動いた先が道0なら動く    0は2進法 00 0 0
動いた先がゴール1なら動く  1は2進法 00 0 1
動いた先が荷物2なら動かない 2は2進法 00 1 0
動いた先が壁6なら動かない  6は2進法 01 1 0

2ビット目が0か1で動くか判定できそうですね。

0x2は「10進法で2」を表します。なので 00 1 0
それをAND演算しています。

道0 and 2なら
0000
0010
:frowning2::frowning2::frowning2::frowning2:
0000

壁6 and 2なら
0110
0010
:frowning2::frowning2::relaxed::frowning2:
0010

さきほどこちらの表記はこういう意味です。

action.js
if((data[dy0][dx0]&0x2)==0){ //行き先が壁でも荷物でもない時
 px = dx0;
  py = dy0; //キャラクターは行き先に行ける
}

else処理として、日本語で書くと

「行き先が荷物の場合、
 さらにその先が道なら、キャラクターを動かす。荷物も動かす

 行き先が荷物の場合、
 さらにその先が壁なら、動かない」

先ほど文をコピペ

動いた先が道0なら動く    0は2進法 00 0 0
動いた先がゴール1なら動く  1は2進法 00 0 1
動いた先が荷物2なら動かない 2は2進法 00 1 0
動いた先が壁6なら動かない  6は2進法 01 1 0

action.js
}else{if((data[dy0][dx0] &0x6)==2)

行き先の値が荷物2で0010 それと0x6をAND演算子で処理をしています。
0x6は10進法6の意味です。つまり2進法なら0110

0010
0110
:frowning2::frowning2::relaxed::frowning2:
0010→2

正直はここはAND演算子の計算を使わず
直接data[dy0][dx0]==2 でいいと思います。

ビット演算の良い所は、
ある一つの部分を調べたら目的の値だけが絞られることです。
「靴を見れば、おしゃれか分かる」
「2ビット目が0か1で、移動できるか分かる」

2ビット目だけを直接取り出す関数がないので、
意図的に計算して、出た結果から判断をしているだけです。

テキストではこのように続きます。

action.js
}else if((data[dy0][dx0] & 0x6)==2){ //行き先が荷物
 if((data[dy1][dx1] & 0x2) ==0){ //さらに先が荷物なし、壁なし
 data[dy0][dx0] ^= 2;
 data[dy1][dx1] |= 2;
  px = dx0;
  py = dy0;
 }
}

ややこしい...
ここでネックとなるのが、ゴールと荷物の関係です。
ゴールの上に置かれた荷物を再度、道側に置いた時に元の場所はゴールに戻しておく必要があります。
map2.jpg

黄色がゴール 青が荷物とします。
荷物がゴールを破壊してはダメなんです。
ゴールと荷物が置かれた状態を別の数字で表現する必要があります。

^は「ビット排他的論理和 (XOR)」です。
|は「ビット論理和 (OR)」です。

ここではまず後者のORから説明します。

OR演算子

data[dy1][dx1]|=2 はdata[dy1][dx1]= data[dy1][dx1] or 2の略です。

道0    0は2進法 0000
ゴール1  1は2進法 0001
荷物2   2は2進法 0010
壁6    6は2進法 0110


道0 OR 2(0010)
0000
0010
:frowning2::frowning2::relaxed::frowning2:
0010→2

ゴール1 OR 2(0010)
0001
0010
:frowning2::frowning2::relaxed::relaxed:
0011 →3

荷物2 OR 2(0010)※この組み合わせはifで弾かれます。
0010
0010
:frowning2::frowning2::relaxed::frowning2:
0010→2

壁6 OR 2(0010)※この組み合わせはifで弾かれます。
0110
0010
:frowning2::relaxed::relaxed::frowning2:
0110 →6


0010 荷物 2
0011 荷物+ゴール 3
1ビット目が0か1で置かれた状況を変えるようなイメージです。

map2.jpg

0011 荷物+ゴール 3を
0001 ゴール 1に戻す方法はないか
 

XOR演算子

data[dy0][dx0] ^= 2 は
data[dy0][dx0] = data[dy0][dx0] ^ 2 の略です。

XORは、すれ違いがあるときが1になる計算です。
1 xor 1 = 0
1 xor 0 = 1
0 xor 1 = 1
0 xor 0 = 0

or演算の違いは、
1 or 1 =1 となります。xorは常にすれ違いの時 だけが 1となります。

2は2進法では0010です。

道0    0は2進法 00 0 0
ゴール1  1は2進法 00 0 1
荷物2   2は2進法 00 1 0
壁6    6は2進法 01 1 0


道0 XOR 2(0010)
0000
0010
:frowning2::frowning2::relaxed::frowning2:
0010→2

ゴール1 XOR 2(0010)
0001
0010
:frowning2::frowning2::relaxed::relaxed:
0011 →3

3は0011でしたので先度計算させると
重なった3 XOR 2(0010)
0011
0010
:frowning2::frowning2::frowning2::relaxed:
0001 →1 ゴール

荷物2 XOR 2(0010)
0010
0010
:frowning2::frowning2::frowning2::frowning2:
0000→0

壁6 XOR 2(0010)
0110
0010
:frowning2::relaxed::frowning2::frowning2:
0100 →4


map2.jpg

ゴールが破壊されずに済みました。

action.js
}else if((data[dy0][dx0] & 0x6)==2){ //行き先が荷物
 if((data[dy1][dx1] & 0x2) ==0){ //さらに先が荷物なし、壁なし
 data[dy0][dx0] ^= 2; //隣の荷物を消す
 data[dy1][dx1] |= 2; //さらに隣に荷物をセットする
  px = dx0;
  py = dy0;
 }
}

描画する

最初に決めたdataのマップの数字を変えるだけなので、
それを元に再描画する処理です。

action.js
function repaint(){
 gc.fillStyle ="black"; //黒い
 gc.fillRect(0,0,800,440); //正方形を描画する

for(var y = 0;y<data.lenght;y++){
 for(var x = 0;x<data[y].lenght;x++){
 if(data[y][x]&0x1){ 
//直接==1 としないのは、ゴールが荷物に上塗りされないようにするため
//ゴール1 0001 ゴールと荷物3 0011 つまり1ビット目が1かどうかで判断される
 gc.drawImage(imgGoal,x*40,y*40,40,40)
}

if(data[y][x]&0x2){
//直接==2とすると、ゴールに荷物がついたときに荷物が消されます。
//荷物0010 荷物とゴール 0011 2ビット目が1かどうかで判断できる
gc.drawImage(imgLuggage,x*40,y*40,40,40)
}

if(data[y][x] == 6){
//壁はどんな時も壁なので、直接
gc.drawImage(imgWall,x*40,y*40,40,40);
}
}
}
gc.drawImage(imgWorker,px*40,py*40,40,40); //キャラクターを描く
}

後書き

if文で直接値を聞いて確認するのに慣れているので、ビット演算子で考える方法は斬新でした。
でもビット演算子で考えると、1ビット目、2ビット目に役割を持てるので判断の幅が広がります。
またゴール1、壁6と適当に割り振ってるのでなく、最初からフローチャートや役割を決めて組み立てて、計算すると都合よいグループで抽出できるようにしているわけです。
ドラマの相関図をあらかじめ考えて、あとは適役を配置する感じ。

直接業務に役立つかわかりませんが、テトリスやマリオにはこの仕組みで計算されていると思うと面白い。
最近配布されたぷよぷよプログラミングも同じ考え方があると思うのでちょっと触ってみます。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1