クリみくじ2021(https://nylon1919.github.io/climikuji2021/)を作りました。クリみくじはかつてはTrouble_SUM(Trouble_SUM | Trouble SUM | Free Listening on SoundCloud)が年始に作っていましたが去年から僕が作ってます。制作のモチベーションはクリオンのクリをいかに面白く見せるかにかかってます。今年もよろしくお願いします。
0.コードについて
自分はいつもPICO-8(PICO-8 Fantasy Console)というエンジンを用いてゲームを作っています。PICO-8の長所は早くて簡単で共有しやすいことです。PICO-8は主にLuaという言語を使いますがプログラムを書く上で面倒な手続き(型宣言など)がいらない割に実行が速く、それはコードが短くていい速さとビルドがすぐできて制作進行がスムーズという速さがあります。また面倒な手続きがいらない為簡単で、HTMLなどにできるので共有もしやすいです。PICO-8には他の制作者のコードを見れる機能もありライブラリとして借りたり参考にすることもできます。今回のコードはそうして勉強してきた内容をベースに一から自作したのでコード全体を紹介していきます。自分一人で制作している為、グローバル変数なども短い変数名にしてたりとか変なクセは見なかったことにしてください。それでは1タブ目から見ていきましょう!
1.1タブ目
--climikuji
function _init()
init_cli()
init_slot()
dmikuji={}
ptcl={}
curs=0
cursp=0
mreset=true
mikuji_reset()
clear=false
shake=4
sfx(63)
music()
end
function _update60()
if clear then
for s in all(slot) do
if s.y<103 then
s.y+=(104-s.y)/4
else
s.y=104
end
end
if (t%30==0) slot[flr((t%120)/30)+1].y=72
t+=1
if (t%30==0) shake=2 sfx(63)
if t>60 then
if btnp(🅾️) then
init_slot()
dmikuji={}
clear=false
mikuji_reset()
mreset=true
shake=5
sfx(63)
end
end
for i=1,2 do
add_ptcl()
end
elseif mreset then
foreach(mikuji,down_mikuji)
local torf=false
for m in all(mikuji) do
if (abs(m.y-48)>0) torf=true
end
mreset=torf
else
if (btnp(⬅️)) curs-=1 sfx(61)
if (btnp(➡️)) curs+=1 sfx(61)
curs%=3
cursp+=1
cursp%=32
if btnp(🅾️) and #dmikuji==0 then
add_dmikuji(mikuji[curs+1])
del(mikuji,mikuji[curs+1])
sfx(62)
c.s=0
if (rnd(4)<1) c.s=flr(rnd(3))*2+2
end
foreach(dmikuji,update_dmikuji)
end
update_cli()
foreach(ptcl,update_ptcl)
end
function _draw()
if shake>0.5 then
local x,y=rnd(2*shake)-shake,rnd(2*shake)-shake
camera(x,y)
shake*=0.95
end
cls()
pal(8,15)
pal(2,4)
for i=0,3 do
spr(32,9+cos(i/4),16+sin(i/4),14,2)
end
pal()
spr(32,9,16,14,2)
if clear then
cls(8)
if (t>240) pal(10,t%4+7) pal(4,t%5)
sspr(32,64,48,32,64-min(64,t/8+16),80-min(128,t/4+16),min(128,t/4+32),min(128,t/4+32))
pal()
circfill(64,256,196,9)
end
foreach(slot,draw_slot)
foreach(mikuji,draw_mikuji)
foreach(dmikuji,draw_dmikuji)
draw_cli()
if not clear then
local x=8+curs*40
rect(x,48,x+31,79,1)
line(x,48+cursp,x,min(79,64+cursp),12)
line(x+31,max(48,63-cursp),x+31,79-cursp,12)
line(x+cursp,79,x+min(16+cursp,31),79,12)
line(max(x,x+15-cursp),48,x+31-cursp,48,12)
if cursp>16 then
line(x,48+cursp-16,x,48,12)
line(x+31,79,x+31,79-cursp+16,12)
line(x+cursp-16,79,x,79,12)
line(x+31,48,x+31-cursp+16,48,12)
end
end
foreach(ptcl,draw_ptcl)
end
function dist(a,b)
return sqrt((a.x-b.x)^2+(a.y-b.y)^2)
end
ぐちゃぐちゃですね。面白いとこだけ説明します。_draw()内のshakeはグローバル変数shakeについてshakeが0.5を超えているときのみ動きます。内容はshake値を減衰させながら乱数で画面に振動を加えるというものです。PICO-8にはcamera()関数があり、camera(x,y)で以降の描画指示を-x,-yするという機能があります。
文字を縁取りたいとき文字を縁取る色でぐるっと描いてからその上に文字を描画します。そのときにぐるっと描くところで場合分けをちまちましたくないので自分はforループで書きます。PICO-8には特別な三角関数が存在し、0~2πでなく0~1を引数に持ってたりsinが正負反転してたりします。それを用いてcos(i/4),sin(i/4)でforループを回すと綺麗に縁取れます。
PICO-8は半年前のアプデで画像の自由変形ができるようになりました。関数はsspr(sx,sy,sw,sh,x,y,w,h)でスプライトのsx,sy番地にある幅sw高さshのスプライトを画面のx,yに幅w高さhで表示します。これを用いて賀正をデカくしたり、クリオンを引き伸ばしたり、くじのめくり機能を演出したりしています。
あとはカーソル枠ですね。ただの四角だと見栄えが悪かったので等速で縁の柄が回るようにしました。rect関数でカーソル位置に正方形を描き、line関数で線を引いて動いているように見せています。このとき線がはみ出さないようmax,minでカットしています。予想通りの挙動が実装でき満足ですが、8行になってはいるものの(最大で8本のline関数が必要な処理の為行数的には最小と思われる)中身が長いのでもう少しシンプルにできそうです。
2.2タブ目
--mikuji
function mikuji_reset()
mikuji={}
for i=0,2 do
local m={}
m.x=8+40*i
m.y=-32
add(mikuji,m)
end
end
function add_dmikuji(m)
local d={}
d.x=m.x+16
d.y=m.y+16
d.w=16
d.h=16
d.theta=0
d.up=false
d.v=360
d.t=0
d.d=0
if slotnum==3 then
d.s=flr(rnd(16))
else
d.s=flr(rnd(14))
end
if rnd(1.5+slotnum*0.5)<1 then
if slotnum==2 then
d.s=3
elseif slotnum==3 then
d.s=15
else
d.s=0
end
end
add(dmikuji,d)
end
function down_mikuji(m)
if mreset then
if abs(m.y-48)>1 then
m.y+=(48-m.y)/4
else
m.y=48
end
else
if m.y<128 then
m.y+=(m.y-47)/8
else
del(mikuji,m)
end
end
end
function update_dmikuji(d)
if d.t<60 then
d.theta+=d.v
d.w=abs(16*cos(d.theta/360))
d.up=(d.theta%360-180)>90
d.v*=0.9
d.t+=1
else
if #mikuji>0 then
d.w=16
d.up=true
foreach(mikuji,down_mikuji)
d.d=dist(slot[slotnum],d)
else
local a=slot[slotnum]
local dd=dist(a,d)
local v=2
if (btn(🅾️) or btn(❎)) v=4
if dd>2 then
d.x+=(a.x-d.x)*v/dd
d.y+=(a.y-d.y)*v/dd
d.w=8+8*(dd/d.d)
d.h=8+8*(dd/d.d)
else
slot[slotnum].f=true
slot[slotnum].s=d.s
slotnum+=1
del(dmikuji,d)
if slotnum<5 then
mreset=true
mikuji_reset()
else
clear=true
t=0
end
shake=2
sfx(63)
end
end
end
end
function draw_mikuji(m)
spr(128,m.x,m.y,4,4)
end
function draw_dmikuji(m)
if m.up then
rectfill(m.x-m.w,m.y-m.h,m.x+m.w-1,m.y+m.h-1,7)
sspr(16*(m.s%8),32+16*flr(m.s/8),16,15,m.x-m.w*0.8,m.y-m.h,1.6*m.w,m.h*2)
else
sspr(0,64,32,32,m.x-m.w,m.y-m.h,m.w*2,m.h*2)
end
end
クリみくじの根幹、みくじ部分です。これを見られると排出率がばれるので隠したかったですが、面白いので見ていきましょう。
まずmikuji_reset関数を見てください。この時点ではx,y座標しか与えられていないので何が出るかは決まってないことになります。そしてadd_dmikuji関数ですがdmikuji(普通のゲームを作るとき処理の関係でenemyとdenemy(敵の死体)をわけるからこの命名)が追加されるのは一タブ目の〇が押されたときにカーソル位置のmikujiを引数に追加されます。そしてx,y座標をコピーした後、ここで何が出るか決めてます。つまり何を選んでも同じです。一応当たりとして「クリオンのクリ」が出てくるようにしていますが、伏せられている3個の中に当たりの「クリ」があるか乱数を生成したとき1/3を引けるかは結果的に同じなのでこういう処理になっています。また「クリオンの」まで引けるとリーチ演出(クリオンが「リーチクリッ!!」と言っている)為、盛り上がるよう最初の「クリ」が1/2、次の「オン」が1/2.5、次の「の」が1/3、次の「クリ」が1/3.5となっていてまあまあ「クリオン」になりやすくしてます。当たる確率は素で乱数を当てない場合は約2%です。実際にモノがあるイメージで作りましたがこれだけ見るとここまで仰々しいコードが無くても同じものは作れそうですね。
dmikujiはめくられると回転しますが先程のsspr関数を用いて横幅を三角関数で伸縮し回転を表現しています。回転角は減速しながら増えて180度を超えると.upがtrueになり表面が描画されます。時間が経つと表を向いたまま止まり、他のみくじをはけさせます。はけるとき等加速度運動をしますが速度は持たず座標情報だけで処理しています。個人的には行数もこの方が少ないし、特定地点の行き来はこの方法の方が制御しやすいです。はけたみくじは消されてdmikujiが下に移動します。下にはslotというテーブルが控えていて最終的に結果を表示するときに使っています。
3.3タブ目
--clion
function init_cli()
c={}
c.x=64
c.y=96
c.w=8
c.h=8
c.t=0
c.theta=0
c.v=3
c.col=7
c.s=0
end
function update_cli()
c.x+=cos(c.theta/720)
c.y+=sin(c.theta/720)
c.t+=1
c.theta+=c.v
if btn(❎) then
if (c.v<22.5) c.v+=0.5
c.col+=1
c.col=c.col%9+7
--[[local a={} a.x,a.y=64,64
local d=dist(c,a)
if d>1 then
c.x+=0.125*(a.x-c.x)/d
c.y+=0.125*(a.y-c.y)/d
end]]
else
if (c.v>3.5) c.v-=0.5
c.col=7
end
if (btn(⬅️) and c.w<128) c.w+=1
if (btn(➡️) and c.w>-128) c.w-=1
if (btn(⬇️) and c.h<128) c.h+=1
if (btn(⬆️) and c.h>-128) c.h-=1
end
function draw_cli()
for i=0,7 do
local x,y=c.x+0.5+c.w*cos(i/8+c.theta/360),c.y+0.5+c.h*sin(i/8+c.theta/360)
local w,h=c.w/3,c.h/3
ovalfill(x-w,y-h,x+w-1,y+h-1,c.col)
end
ovalfill(c.x-c.w,c.y-c.h,c.x+c.w-1,c.y+c.h-1,c.col)
palt(15,true)
palt(0,false)
sspr(c.s*8,0,16,16,c.x-c.w,c.y-c.h,c.w*2,c.h*2)
local x,y=c.x-c.w*1.4,c.y+4
if clear then
pal(14,t%8+7)
spr(224,x,y,15,1)
elseif slotnum==1 then
spr(192,x,y,11,1)
elseif slotnum==2 then
spr(208,x,y,9,1)
elseif slotnum==3 then
spr(240,x,y,13,1)
elseif slot[1].s==0 and slot[2].s==3 and slot[3].s==15 then
spr(217,x,y,7,1)
else
spr(203,x,y,5,1)
end
pal()
palt()
end
3タブ目はクリオンについてです。クリオンは顔だけスプライトで他は楕円の描画関数でできています。クリオンの鬣は速度をもらって回転していますが×ボタンで加速するようにできています。クリオンは制御できるようにしたかったのですが、めんどかったので伸びたり縮んだり以外は勝手に回っていて画面から消えると戻って来たり来なかったりします。クリオンの発言は6通りあります。
4.4タブ目
--slot
function init_slot()
slot={}
slotnum=1
for i=0,3 do
local s={}
s.x=37+i*18
s.y=104
s.f=false
add(slot,s)
end
end
function draw_slot(s)
if s.f then
rectfill(s.x-9,94,s.x+8,113,6)
rectfill(s.x-9,s.y-10,s.x+8,s.y+9,7)
sspr(16*(s.s%8),32+16*flr(s.s/8),16,15,s.x-8,s.y-8,16,16)
end
end
4タブ目はスロットについてですね。スロットは全部引いた後の処理は1タブ目で済ませてるのであんま言うことないです。
5.5タブ目
--ptcl
function add_ptcl()
local p={}
p.x=16+flr(rnd(2))*96+rnd(48)-24
p.y=128
p.vx=(64-p.x)/8
p.vy=-4-rnd(2)
p.col=flr(rnd(8))+7
p.t=flr(rnd(360))
add(ptcl,p)
end
function update_ptcl(p)
p.x+=p.vx
p.y+=p.vy
p.vx*=0.8
if (p.vy<1) p.vy+=0.25
if (abs(p.vx)<0.25 and p.vy>0) p.vx+=cos(p.t/360)/8
p.t+=1
if (p.y>128) del(ptcl,p)
end
function draw_ptcl(p)
pset(p.x,p.y,p.col)
end
5タブ目はパーティクルについてです。みくじを引き終わるとパラパラ舞ってるあれです。このパーティクルがなんだかんだ一番制御に悩みました。最初は勢いよく左右から飛び出して真ん中らへんでひらひら舞うという挙動をそれなりにシンプルにまとめたつもりです。どれも真ん中に向かって飛び、徐々に速度を失い、重力によって下降し、終端速度はほどほどに遅く、横方向の速度が弱ければ三角関数で振動するというだけですが、綺麗に舞ってるように見えます。いいですね。
6.まとめ
みんなは「クリオンのクリ」じゃなくて(1/14)^4で「コリコリオナホッ」とか引けるように頑張ろう。