プログラムでマリオを作るドカンの移動処理

ドカン移動処理の実装(プログラミングでマリオを作る42回)

また、半年空いて今回はドカンの移動処理の実装をします。

実装結果は以下のようになります。

マリオドカン移動処理完成

ドカンでの移動処理を行い、その後にマップの切り替えを行っています。

では、今回実装する新たな内容をまとめます。

今回実装する内容

1.マップの切り替え処理

2.アニメーション処理

3.キーの入力の無効・有効処理

4.y軸方向の当たり判定の修正(移動量を制限する)

マップの切り替え処理は未実装ですが、基本的に今までやってきたことの応用になります。

では、実装していきましょう。

マップ切り替え処理の実装

マップを切り替えるためにどうしたらいいかということを考えてみます。

まず、マップを切り替えると、敵の処理とオブジェクトや背景を分けなければいけません。

ただ、背景の画像を切り替えるだけでは、前のマップにいた敵が描画され、動いている状態になってしまいます。

なので、ここではマリオがどこのマップにいるかを表す変数を設けて、
その値に応じて、敵の表示や動きの処理を分けたいと思います。

その変数をマリオクラスにおくと、参照するのがめんどくさいので、
新しくglobal.jsを作り、そこにグローバル変数として、マップの状態を管理する変数を作成します。

global.js

let gMapStage = MAP_ONE;

次に、const.jsにマップ定数などを追加します。

このマップ定数をマップ状態変数に代入することによって、マップごとの表示や動作などを切り替えます

const.js

// chapter42
// マップ番号はオブジェクトや敵などのindexに対応させる必要がある
let MAP_ONE = 0;
let MAP_TWO = 1;
let MAP_THREE = 2;
let DOCAN_LEFT = 0;
let DOCAN_UP = 1;
let DOCAN_RIGHT = 2;
let DOCAN_DOWN = 3;
// 土管を移動する時の遷移時間frame換算
let DOCAN_MOVE_TIME = 120;

例えば、gMapStageにMAP_ONEを代入すれば、クリボ配列の0番目の
クリボ達が描画され、ドカン配列の0番目のドカン達が参照されるというわけです。

では、例として、クリボを2重配列にした例を見てみます。

let gKuribos = [
  [new Kuribo(1024,384,LEFT_DIR),new Kuribo(1088,384,LEFT_DIR)],
  []
];

描画や動作関数はこんな感じになります。

for(var i = 0;i < gKuribos[gMapStage].length;++i){
  gKuribos[gMapStage][i].draw(g_Ctx,gKuriboTex,gMario.mapScrollX);
}

配列の長さをみることで、配列の値が空の場合、null参照されないことになります。

続いて、マップチップ配列について考えます。

マップチップ配列も同様に配列にしてもいいんですが、
読みにくくなるので、switchで分けるようにしました。

もちろん3次元配列にしてもだいじょうぶです。

// 背景
switch(gMapStage){
  case MAP_ONE:
    drawMap(gBackGroundMapChip);
    break;
  case MAP_TWO:
    drawMap(gBonusBackMapChip);
    break;
}

gBonusBackMapChipというgif画像に表示されているドカン遷移後のマップチップを新しく定義しています。

マップの切り替えについては、だいたいこんな感じです。

ドカン移動処理

続いて、ドカン移動処理の実装に移ります。

ここで、土管はマリオよりも手前に描画する必要があるので、
オブジェクト用のマップチップを手前に描画するか、マップチップとは別に
ドカンを描画するかしないといけません。

ここでは、描画専用のドカンクラスを作って、マリオよりも手前に描画させ、
当たり判定の役割として、オブジェクト用のマップチップに土管を組み込むことにします。

では、ドカンクラスようのjsファイルを新しく作成します。

docan.js

/**
 * 土管クラス
 * @param {*} posX : 土管位置x
 * @param {*} posY : 土管位置y
 * @param {*} width : 土管の幅
 * @param {*} height : 土管の高さ
 * @param {*} mapNumber : 遷移先のマップチップ番号 
 * @param {*} mapSizeX :    Xのマップサイズ 
 * @param {*} firstDirection : 入り口上下左右どちらに移動するか
 * @param {*} endDirection : 出口上下左右どちらに移動するか
 * @param {*} endX : 遷移先の土管の位置X
 * @param {*} endY : 遷移先の土管の位置Y
 */
function Docan(posX,posY,width,height,mapNumber,mapSizeX,firstDirection,endDirection,endX,endY){
    this.posX = posX;
    this.posY = posY;
    this.width = width;
    this.height = height;
    this.mapNumber = mapNumber;
    this.mapSizeX = mapSizeX;
    this.firstDirection = firstDirection;
    this.endDirection = endDirection;
    this.endX = endX;
    this.endY = endY;
}

/**
 * 描画専用のドカンオブジェクト
 * @param {*} posX 
 * @param {*} posY 
 * @param {*} direction 
 */
function DocanObj(posX,posY,direction){
    this.posX = posX;
    this.posY = posY;
    this.direction = direction;
}

/**
 * 描画関数
 * @param {*} ctx 
 * @param {*} texture 
 * @param {*} scrollX 
 */
DocanObj.prototype.draw = function(ctx,texture,scrollX){
    switch(this.direction){
        case DOCAN_LEFT:
            ctx.drawImage(texture,320,96,64,64,this.posX - scrollX,this.posY,MAP_SIZE * 2,MAP_SIZE * 2);		    
        //ctx.drawImage(texture,this.starOffsetX + (this.animX * 32) + this.animOffsetX,(this.direction * this.height) + this.textureOffsetY,32,this.height,this.posX,this.posY,32,this.height);		    
            break;
        case DOCAN_UP:
            ctx.drawImage(texture,448,96,64,64,this.posX - scrollX,this.posY,MAP_SIZE * 2,MAP_SIZE * 2);		    
            break;
        case DOCAN_DOWN:
            ctx.drawImage(texture,384,96,64,64,this.posX - scrollX,this.posY,MAP_SIZE * 2,MAP_SIZE * 2);		    
            break;
        case DOCAN_RIGHT:
            break;  
    }
}

Docanクラスをドカン遷移用の判定に使い、DocanObjクラスは描画用に保持しています。

ドカンの処理はマリオの方で行うので、main.jsでドカンクラスを変数として宣言しておき、マリオのupdate関数に渡して、そこでドカンクラスを参照して、
処理を行うようにします。

ドカンクラスをmain.jsで以下のように保持します。

// マップによって変化できるように配列で持つ
let gDocans = [
  [new Docan(384,320,64,64,MAP_TWO,gBonusMapChip[0].length,DOCAN_DOWN,DOCAN_DOWN,48,64)],
  [new Docan(576,384,64,64,MAP_ONE,gMapChip[0].length,DOCAN_RIGHT,DOCAN_UP,2768,320)]
];

// マップによって変化できるように配列で持つ
let gDocanObjs = [
  [new DocanObj(384,352,DOCAN_UP),new DocanObj(2756,352,DOCAN_UP)],
  [new DocanObj(32,64,DOCAN_DOWN),new DocanObj(576,384,DOCAN_LEFT)]
];

このドカンクラスをマリオのupdate関数で渡すようにします。

続いて、マリオクラスにドカン移動のための実装を追加します。

マリオクラスにドカンの処理を追加する

マリオクラスの変数に以下のような変数を追加します。

// chapter42
this.docanMoveCnt = 0;			// 土管移動用カウンター
this.mapMoveStage = MAP_ONE;		// 遷移先マップ
this.docanfirstMoveDirection = DOCAN_UP;	// 移動方向
this.docanEndMoveDirection = DOCAN_UP;	// 移動方向
this.isMapMove = false;			// map移動中フラグ
this.isSecondMapMove = false;		// 土管から出てくる時のフラグ
this.docanPosX1 = 0;				// 最初に遷移するX
this.docanPosY1 = 0;				// 最初に遷移するY
this.docanPosX2 = 0;				// 遷移後のx
this.docanPosY2 = 0;				// 遷移後のy
this.newMapSizeX = 0;				// 新規マップのマップ描画量
this.keyDisable = false;			// キー入力を受け付けないフラグ

変数の説明はコメントに書いてあるので、参照してください。

ドカンに入ることができるか判定する関数

続いて、ドカンに入ることができるか判定する関数を作ります。

下に入る土管に入ることができるのは左右がドカンの範囲内(+offset)かつマリオの下と、ドカンの上の面が設置している場合です。

/**
 * chapter42 ドカンが下方向に突入できるか判定
 * @param {*} docan 
 */
Mario.prototype.docanDownEnter = function(docan){
	let offset = 8
	// x軸
	if(this.moveNumX >= docan.posX + offset && this.moveNumX + 32 <= docan.posX + docan.width - offset){
		// デカくなったときはサイズが変わる
		let tall = this.isBig() ? 32 : 0;
		// y軸
		if(this.posY + tall == docan.posY){
			this.setDocanParam(docan);
		}
	}
}

入れることでできると判定されたときに、ドカン移動用の状態をsetDocanParam関数で設定するという流れになります。

続いて、x軸方向の判定をみます。

/**
 * 
 * @param {土管クラス} docan 
 * @param {マリオの移動方向} direction 
 */
Mario.prototype.docanXEnter = function(docan,direction){
	// y軸:地面についていないと侵入できない
	if(this.posY + this.height == docan.posY + docan.height){
		// マリオが左方向かつ土管が左方向に進む場合
		if(direction == LEFT_DIR && docan.firstDirection == DOCAN_LEFT){
			// x座標
			if(this.moveNumX == docan.posX + docan.width){			
				this.setDocanParam(docan)
			}
		}
		// マリオが右方向かつ土管が右方向に進む
		else if(direction == RIGHT_DIR && docan.firstDirection == DOCAN_RIGHT){
			// x座標
			if(this.moveNumX + MAP_SIZE == docan.posX){
				this.setDocanParam(docan)
			}
		}
	}
}

x軸方向は一度に判定しているため、引数directionによって入れる方向を分けています。

続いて、ドカンの移動パラメータをセットする、setDocanParam関数を実装します。

ドカンの移動用パラメータをセットする関数

setDocanParamの中身を実装します。

関数の流れとしては、

1.キー判定やマップチップとの当たり判定を無効にするフラグを立てる

2.土管移動後の座標や、マップチップ情報などを渡す

などの設定をしています。

/**
 * 土管移動用のパラメータをセットする
 * @param {*} docan 
 */
Mario.prototype.setDocanParam = function(docan){
	this.isMapMove = true;		// マップ移動中フラグ(当たり判定などをおこさせない)
	this.keyDisable = true;		// キー操作無効フラグ
	this.mapMoveStage = docan.mapNumber;		// マップ移動番号
	this.docanFirstMoveDirection = docan.firstDirection;	// ドカンに入る際にマリオがに移動する方向
	this.docanEndMoveDirection = docan.endDirection;		// ドカンから出る際ににマリオが移動する方向
	this.isSecondMapMove = false;		// 入る際はfalse,出る際はtrue
	this.newMapSizeX = docan.mapSizeX;	// 新しいマップのXサイズスクロール判定に使用する
	// でかい時にずれる量を調整する
	let height = this.isBig() ? 32 : 0;

	// 土管の出る位置によってスタート地点を変える
	switch(this.docanEndMoveDirection){
		case DOCAN_UP:
			// 出てくる位置を2つ下げる
			this.docanPosY1 = docan.endY + (MAP_SIZE * 2) - height;
			this.docanPosX1 = docan.endX;
			this.docanPosY2 = docan.endY - height;
			break;
		case DOCAN_DOWN:
			this.docanPosY1 = docan.endY - (MAP_SIZE * 2) - height;
			this.docanPosX1 = docan.endX;
			this.docanPosY2 = docan.endY;
			break;
		case DOCAN_RIGHT:
			this.docanPosX1 = docan.endX - (MAP_SIZE * 2);
			this.docanPosY1 = docan.endY;
			this.docanPosX2 = docan.endX;
			break;
		case DOCAN_LEFT:
			this.docanPosX1 = docan.endX - (MAP_SIZE * 2);
			this.docanPosY1 = docan.endY;
			this.docanPosX2 = docan.endX;
			break;
	}
}

そして、ドカン移動処理を行うために、update関数内でドカン移動用関数を呼びます。

ドカン移動用関数

isSecondMapMove変数を使って、入り口か出口かの処理を分けています。

ドカンのみマリオの前方に描画されているので、マリオその範囲内に納めるための処理も入れています。

今回の場合ですと、遷移後のマップはスクロールしないので、
スクロール位置を管理する変数も更新する必要があります。

最初の移動が終わった後に、マップを管理する変数を更新して、
マップを遷移するようにしています。

/**
 * chapter42
 * 
 * ドカン移動関数
 */
Mario.prototype.docanMove = function(){
	if(this.isMapMove){
		// 一回目の移動
		if(!this.isSecondMapMove){
			// マップ移動初期処理
			if(this.docanMoveCnt++ >= DOCAN_MOVE_TIME){
				this.docanMoveCnt = 0;
				this.changeMap(this.mapMoveStage,this.newMapSizeX);	
				this.posY = this.docanPosY1;
				this.moveNumX = this.docanPosX1;
				this.mapScrollX = 0;
				if(this.moveNumX >= SCROLL_POINT_X && this.moveNumX < this.scrollEndX)
				{
					this.mapScrollX = this.moveNumX - SCROLL_POINT_X;		// マップスクロール量
					this.posX = SCROLL_POINT_X;							// 固定
					// マップを描画する範囲をずらす
					this.maxDrawMapX = DRAW_MAX_MAP_X + Math.floor(this.mapScrollX / MAP_SIZE);			// 最大の描画範囲X
					this.minDrawMapX = this.maxDrawMapX - DRAW_MAX_MAP_X;								// 最小の描画範囲X
				}
				else{
					this.mapScrollX = 0;
					this.posX = this.docanPosX1;
					this.maxDrawMapX = DRAW_MAX_MAP_X;	// 最大の描画範囲X
					this.minDrawMapX = 0;				// 最小の描画範囲X
				}
				this.isSecondMapMove = true;				
			}
			else{
				// 土管からはみ出さないように最大移動量は64まで
				if(this.docanMoveCnt <= 32){
					// 移動
					switch(this.docanFirstMoveDirection){
						case DOCAN_UP:
							this.posY -= 2;
							break;
						case DOCAN_DOWN:
							this.posY += 2;
							break;
						case DOCAN_LEFT:
							this.moveNumX -= 2;
							// 歩いている動作をさせる
							this.increseAnimCnt(2);
							break;
						case DOCAN_RIGHT:
							this.moveNumX += 2;
							// 歩いている動作をさせる
							this.increseAnimCnt(2);
							break;
					}	
				}
			}
		}
		// 2回目(土管から出る場合)
		else{
			// 2秒で移動させたい、64 / 2 = 32フレで移動できるので、32引いてから移動を開始する
			if(this.docanMoveCnt++ >= DOCAN_MOVE_TIME - 32){
				// 移動方向を分ける
				switch(this.docanEndMoveDirection){
					case DOCAN_UP:
						this.posY -= 2;
						if(this.posY <= this.docanPosY2){
							this.resetMapMove();
						}
						break;
					case DOCAN_DOWN:
						this.posY += 2;
						if(this.posY >= this.docanPosY2){
							this.resetMapMove();
						}
						break;
					case DOCAN_LEFT:
						this.moveNumX -= 2;
						this.increseAnimCnt(2);
						if(this.moveNumX >= this.docanPosX2){
							this.resetMapMove();
						}
						break;
					case DOCAN_RIGHT:
						this.moveNumX += 2;
						this.increseAnimCnt(2);
						if(this.moveNumX <= this.docanPosX2){
							this.resetMapMove();
						}
						break;
				}
			}
		}
	}
}

続いて、この関数内に新しく定義している、3つの関数の中身を実装します。

increseAnimCnt関数については、歩く関数で実装していた中身を
関数にしただけです。

/**
 * マップを変更する
 */
Mario.prototype.changeMap = function(mapNumber,length){
	gMapStage = mapNumber;
	// 描画範囲を変更
	this.scrollEndX = (length - 10) * MAP_SIZE - HALF_MAP_SIZE;		// スクロールの終わりとなる終点X
}

/**
 * 
 * @param {*移動アニメーションカウントを増やす} cnt 
 */
Mario.prototype.increseAnimCnt = function(cnt){
	this.animCnt += cnt;
	// animation
	if(this.animCnt >= 12){
		this.animCnt = 0;
		// 一定以上に達したらアニメーションを更新する
		if(++this.animX > 3){
			this.animX = 0;
		}
	}
}

/**
 * 土管の移動終了後にセットする変数
 */
Mario.prototype.resetMapMove = function(){
	this.isMapMove = false;
	this.keyDisable = false;
	this.isSecondMapMove = false;
	this.docanMoveCnt = 0;
	this.updateMapPosition();
}

次に、キー入力や土管移動中にマップチップとの当たり判定を起さないよう
対象関数にフラグ変数で制御します。

		if(!this.keyDisable){
			if(gLeftPush){
				for(var i = 0;i < docans.length;++i){
					this.docanXEnter(docans[i],LEFT_DIR);
				}
				if(gSpacePush){
					this.setIsDash(true);
					this.moveX(mapChip,-DASH_SPEED);
				}
				else{
					this.setIsDash(false);
					this.moveX(mapChip,-NORMAL_SPPED);
				}
			}
		}

マップチップとの当たり判定を無効にする

isMapMoveフラグを利用します。

Mario.prototype.collisionY = function(map,posY){
	if(!this.isMapMove){

Mario.prototype.collisionX = function(map,posX){
	// マップ移動中は当たり判定を起こさない
	if(!this.isMapMove){

Y軸方向の当たり判定を修正する

動かしていてわかったんですが、マリオが特定の加速度かつ特定の位置にいたときに、下のマップチップとのすり抜け判定があるパターンがあることに気づいたので、その修正をします。

修正方法は、以前実装した敵キャラのすり抜け処理と同様に移動量を2分割して、当たり判定を行うというものです。

では、中身をみてみます。

	// 移動量を2分割する
	for(var i = 2;i > 0;--i){
		this.addPosY = parseInt(this.jumpPower / i);
		// 当たり判定があった場合は抜ける
		if(this.collisionY(mapChip,this.posY - this.addPosY)){
			break;
		}
	}
	this.posY -= this.addPosY;

まず、移動量の半分で当たり判定を行わせて、collisionY関数で当たり判定が行われた、すなわちtrueが返ってきたときに当たり判定処理を抜けて、Y軸方向の移動量を調整するようにします。

なので、collisionY関数では、当たり判定が行われたときに、trueを返すようにします。

/**
 * Y軸方向の当たり判定
 * 
 * @param {*} map 
 * @param {*} posY 
 * 
 * return : 当たり判定の有無
 */
Mario.prototype.collisionY = function(map,posY){
	if(!this.isMapMove){
		this.updateMapPositionY(posY);
		// マップ座標xを配列で保管する
		var mapsX = [this.rightMapX,this.leftMapX];
		for(var i = 0;i < 2;++i){
			// マリオの上側に当たった場合
			if(isObjectMap(map[this.upMapY][mapsX[i]])){
				// コインブロックだった場合
				if(isCoinBlock(map[this.upMapY][mapsX[i]])){
					// コインブロック用のアニメーションをセットする
					var coinX = mapsX[i] * MAP_SIZE;
					// 一つ上にセットする
					var coinY = (this.upMapY - 1) * MAP_SIZE;
					this.setBlockCoinMapAnim(i,coinX,coinY);
					// ボックスを空にする
					replaceEmptyBoxMap(map,mapsX[i],this.upMapY);
					// coinの取得
					this.getCoin();
				}

				// キノコブロックだった場合(chapter34&38)
				if(isKinokoBlock(map[this.upMapY][mapsX[i]])){
					// chapter40 キノコを有効化する処理を関数にした
					var kinokoPosX = mapsX[i] * MAP_SIZE;
					var kinokoPosY = this.upMapY * MAP_SIZE;
					this.activateKinoko(kinokoPosX,kinokoPosY,LEFT_DIR);
					// ボックスを空にする
					replaceEmptyBoxMap(map,mapsX[i],this.upMapY);
				}

				// starブロックだった場合(chapter41)
				if(isStarBlock(map[this.upMapY][mapsX[i]])){
					// chapter40 キノコを有効化する処理を関数にした
					let starPosX = mapsX[i] * MAP_SIZE;
					let starPosY = this.upMapY * MAP_SIZE;
					this.star.activate(starPosX,starPosY,LEFT_DIR);
					// ボックスを空にする
					replaceEmptyBoxMap(map,mapsX[i],this.upMapY);
				}      
				
				// ブロックのアニメーション
				if(isBlockMap(map[this.upMapY][mapsX[i]])){
					var posX = mapsX[i] * MAP_SIZE;
					var posY = this.upMapY * MAP_SIZE;
					this.blockAction(mapsX[i],this.upMapY,false,map);
				}

				// (加算される前の)中心点からの距離をみる
				var vecY = Math.abs((this.posY + HALF_MAP_SIZE) - ((this.upMapY * MAP_SIZE) + HALF_MAP_SIZE));
				// Yの加算量調整
				this.addPosY = Math.abs(MAP_SIZE - vecY);
				// 落下させる
				this.jumpPower = 0;
				return true;
			}
		}
		// マリオの下側
		if(isObjectMap(map[this.downMapY][this.rightMapX]) || isObjectMap(map[this.downMapY][this.leftMapX])){
			// (加算される前の)中心点からの距離を見る
			var centerY = this.height == 64 ? this.posY + 32 : this.posY;
			var vecY = Math.abs((centerY + HALF_MAP_SIZE) - ((this.downMapY * MAP_SIZE) + HALF_MAP_SIZE));
			// Yの加算量調整
			this.addPosY = Math.abs(MAP_SIZE - vecY);
			// 地面についた
			this.posY += this.addPosY;
			this.addPosY = 0;
			this.jumpPower = 0;
			this.isJump = false;
			// リセットアニメーション
			this.animOffsetX = 0;
			return true;
		}
	}
	return false;
}

以上です。

今回のコード

githubに上げているので、参照してください。

まとめ

文章も作る時間も長くなってしまった。

マップチップのところは、3重配列にしてもよかったかなと思う。

まぁこんなもんでしょうか。

まだ結構やることが残っているので、完成までは遠いなぁ...

まぁ一気にやっちゃえばすぐ終わりそうですが。