PICO-8で横スクロールステージを作る

スクロールはロマン

ゲームを作るなら、一度は作ってみたい「スクロールするステージ」。
PICO-8では、複雑でなければ比較的簡単にスクロール演出を実装することが可能です。

ここでは「ジャンプアクションの基本を作る」をベースに、コードを整理しながら横スクロールのステージを作成する手順を紹介します。

まずコードを整理する

ジャンプアクションの基本を作る」では、プレイヤーに関するコードがすべて、_init()、_update()、_draw()のメインループに直接書かれて、変数もすべてグローバル変数になっていました。

現時点ではプレイヤーしかいないので大きな問題ではないですが、今後いろいろ付け足すことを考えると、プレイヤーのための変数は上書きされないよう束ねて置いておき、動きも関数にまとめて修正や呼び出しを簡単にしたいところです。

プレイヤーの変数をまとめる

function _init()
	x=0-- X座標
	y=8-- y座標
	dx=0-- 横移動量
	dy=0-- 縦移動量
	counter=1-- アニメーション用カウンター
	spr_num={4}-- 初期スプライト(ジャンプポーズ)
	gravity=0.5-- 重力加速
	max_gravity=6-- 最大落下速度
	jumping=true-- ジャンプ中かどうかのフラグ
	max_height=6-- ジャンプの最大高
end

_init()で定義している変数の中で、プレイヤーに関係するものは、x, y, dx, dy, counter, spr_num, jumpingです。

コード編集画面で新しいタブをつくり、必要な変数を_init()からカットして、次のような形になるようペーストします。

-- player
player={
	x=16,
	y=64,
	dx=0,
	dy=0,
	counter=1,
	spr_num={4},
	jumping=true
}

player」という名前のテーブルの中に、変数をまとめました。
変数はテーブルの要素になるので、それぞれ「,」で区切ります(区切らないとエラーになる)。

プレイヤーの動きを関数にする

同じ要領で、_update()からもコードを移動します。
_update()はすべてプレイヤーに関する内容なので全てをカットし、同じタブに次のような形になるようペーストして、player_move()という変数にします。

function player_move()
	-- horizontal
	player.dx=0
	if btn(0) then
		player.dx-=2
		player.f=true
	end
	if btn(1) then
		player.dx+=2
		player.f=false
	end
	if btn(🅾️) and
	not player.jumping then
		player.dy-=max_height
		player.jumping=true
	end
	
	player.dy=
	min(player.dy+gravity,max_gravity)
	
	-- horizontal
	if h_collision(player.x+player.dx,player.y) then
		player.dx=0
	end
	
	-- heading
	if player.jumping
	and heading(player.x+player.dx,player.y+player.dy) then
		player.dy=1
	 player.y=flr((player.y+player.dy+1)/8)*8
	end
	
	-- landing
	if landing(player.x+player.dx,player.y+player.dy) then
	 player.y=flr((player.y+player.dy+7)/8)*8-8
	 player.dy=0
		player.jumping=false
	end
	
	-- in midair
	if player.dy>1 then
		player.jumping=true
	end
	
	-- move
	player.x+=player.dx
	player.y+=player.dy
	--player.x=mid(0,player.x,120)
	
	-- animation frame
	if not player.jumping then
		player.spr_num={1,2,3,2}
	else
		player.spr_num={4}
	end 
	if player.dx!=0 then
		player.counter+=0.5
	end
end

このコードから参照していた変数は、もともとは_init()の中で定義されていたグローバル変数(どこからでもアクセスできる変数)です。
しかし今回、プレイヤーの変数は「player」テーブルにまとめたので、参照先が変わっています。

テーブルの中の変数を読み出すときは、テーブル名.変数名となるよう、変数名の前に「.」で繋いでテーブル名を付け加えます。
変数はすべてplayerテーブルに入っているので、player_move()の中にある変数の前に「player.」を追加しました。

最後に、_draw()からもコードを持ってきます。
_draw()にはcls()だけを残して、player_move()の下にplayer_draw()を作成します。

こちらも同様に、変数名の前にplayer.を追記しています。

function player_draw()
	local idx=
	flr(player.counter%#player.spr_num)+1
	spr(player.spr_num[idx],
	player.x,player.y,1,1,player.f)
end

メインループを修正する

コードを移動したので、メインループは次のようにすっかり短くなりました。

function _init()
	gravity=0.5
	max_gravity=6
	max_height=6
end

function _update()
end

function _draw()
	cls()
end

ここに、先ほど定義した関数を書き込みます。

function _init()
	gravity=0.5
	max_gravity=6
	max_height=6
end

function _update()
	--プレイヤーを動かす
	player_move()
end

function _draw()
	cls()
	--プレイヤーを描く
	player_draw()
end

この状態で実行すると、元と同じように動作するはずです。

メインループが見やすくなったところで、スクロールを実装してみましょう。

camera()を使って画面をスクロールさせる

画面をスクロールさせるのに最も簡単な方法は、camera()を使うことです。

CAMERA([X, Y])

Set a screen offset of -x, -y for all drawing operations

CAMERA() to reset

cameraの概念

camera()は画面位置のオフセット(-x, -y)を指定する関数、つまり、PICO-8世界のどこを切り取って見せるかを決める機能といえます。

camera(100,0)とすると、デフォルトの場所から右に100px移動した先の絵が表示されます。
これは表示する(画面に切り取る)場所が変わっただけなので、PICO-8世界に表示されているキャラクターの絶対位置は変わりません
たとえば、(20,80)の座標にキャラクターがいる状態でcamera(100,0)としても、キャラクターの位置はそのまま、ただ、表示の外にいて見えなくなるというだけです。

スクロールのサンプルその1(プレイヤーに常に追従する)

サンプルのcartを用意しました。
この画像(gravity_and_jumping_scroll1.p8.png)をダウンロードして、PICO-8で開いてください。

少しスプライトを追加して、簡単なマップが設定してあります。

camera()は_draw()の中で使います。

function _draw()
	cls()
	camera(player.x,0)
	map(0,0,0,0,48,16)
	--draw player
	player_draw()
end

幅48セル分のマップを表示して、カメラのX座標と、プレイヤーのX座標が同期するようにしてあります。

動かしてみると、プレイヤーの動きに合わせて画面がスクロールします。

ただ、このままだと端に張り付いてちょっと気持ち悪いですね。

スクロールのサンプルその2(画面の真ん中で追従する)

サンプル2

一般にある横スクロールゲームのように、プレイヤーがある地点まで来たらスクロールを始めるように改造します。

この画像(gravity_and_jumping_scroll2.p8.png)をダウンロードして、PICO-8で開いてください。

カメラの部分に少し手を加えます。

-- camera
cam={
	x=0,
	y=0
}
function cam_update()
	if player.x>63 then
		cam.x=player.x-64
	end
end

タブ3に、カメラの座標のテーブル(cam)と、それを計算する関数(cam_update)を設定しました。
_update()の中でカメラの位置を設定、camera()にはcam.xでX座標を指定します。

function _update()
	--move player
	player_move()
	cam_update()-- カメラの位置を設定
end

function _draw()
	cls()
	camera(cam.x,0)-- カメラ専用の座標で表示
	map(0,0,0,0,48,16)
	--draw player
	player_draw()
end

プレイヤーのX座標が63(画面の真ん中)を超えているとき、カメラのX座標とプレイヤーのX座標を同期させます。
ただそのまま一緒の数字にすると、スクロールが始まると同時にまたプレイヤーが画面の端にいる状態になってしまうので、カメラの位置は、プレイヤーが移動したを分引いて、真ん中に表示されるよう調整しています。

どうでしょう?
かなりゲームっぽい画面になってきました。

スプライトやマップ、プレイヤーのパラメーターを書き換えて、遊んでみてください。

おまけ(多重スクロール)

多重スクロール

camera()は一度しか使えないわけではありません。

多重スクロールサンプル

この画像(gravity_and_jumping_scroll3.p8.png)をダウンロードして、PICO-8で開いてください。

何かを描画する前に、それぞれ座標の違うcamera()を実行しておくと、違う座標にあるものを一つの画面に表示することができます。

これを使うと、サンプルのように多重スクロールのような効果が可能です。

スコアや残機表示など、スクロールさせたくないものは、camera()もしくはcamera(0,0)とした後に描画すると、スクロールしないで同じ場所に表示されます。

function _draw()
	cls(1)
	camera(cam.x*0.4,0)-- 遠景のカメラ座標。0.4をかけて移動を遅くしている
	map(0,16,0,20,30,20)
	camera(cam.x,0)-- 近景のカメラ座標
	map(0,0,0,0,stage_width,16)
	--draw player
	player_draw()
	--text
	camera()-- ステータス表示のカメラ座標
	print("this is demo",2,120,7)
end

まとめ

  • camera() を使えば、簡単に横スクロールステージを作れる
  • camera() の座標調整で自然な追従やUI固定も実現可能
  • スクロールは楽しい!