クリみくじ2021を作りました

クリみくじ2021(https://nylon1919.github.io/climikuji2021/)を作りました。クリみくじはかつてはTrouble_SUM(Trouble_SUM | Trouble SUM | Free Listening on SoundCloud)が年始に作っていましたが去年から僕が作ってます。制作のモチベーションはクリオンのクリをいかに面白く見せるかにかかってます。今年もよろしくお願いします。

f:id:himawarifurutani1919:20201231143713p:plain

作ったスプライト全体

 

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というテーブルが控えていて最終的に結果を表示するときに使っています。

f:id:himawarifurutani1919:20201231143921g:plain

 

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通りあります。

f:id:himawarifurutani1919:20201231144128g:plain

 

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タブ目はパーティクルについてです。みくじを引き終わるとパラパラ舞ってるあれです。このパーティクルがなんだかんだ一番制御に悩みました。最初は勢いよく左右から飛び出して真ん中らへんでひらひら舞うという挙動をそれなりにシンプルにまとめたつもりです。どれも真ん中に向かって飛び、徐々に速度を失い、重力によって下降し、終端速度はほどほどに遅く、横方向の速度が弱ければ三角関数で振動するというだけですが、綺麗に舞ってるように見えます。いいですね。

f:id:himawarifurutani1919:20201231144906g:plain

 

6.まとめ

みんなは「クリオンのクリ」じゃなくて(1/14)^4で「コリコリオナホッ」とか引けるように頑張ろう。