🎮

【PICO-8】PICO-8でオブジェクト指向プログラミングっぽいブロック崩しを作る

に公開

はじめに

PICO-8という、ミニマムなゲームを制作できるツールを使って、オブジェクト指向を利用したブロック崩しを作ってみました。

その際のナレッジをこちらの記事にまとめます。

PICO-8概要

https://www.lexaloffle.com/pico-8.php

  • 128x128の解像度
  • 色数12種類のみ
  • コードにもトークンという単位で、文字数制限がある
    などの制約で、ミニマムなゲームを制作できる

PIC-8におけるオブジェクト指向

結論ですが、トークンという文字数制限があるため、オブジェクト指向が必ずしもベストプラクティスとは言えないのがPICO-8の面白いところです。

利便性、可読性が向上するメリットがありますが、クラス(メタテーブル)の定義などにトークンを消費してしまいます。

celesteなどの中~大規模ゲームは、トークン制約によって使いたくても使えない場合があります。
今回のようなブロック崩しのような小規模ゲームで利用できるかもしれません。

メタテーブル

メタテーブルという機能を利用することで、本格的なオブジェクト指向なコーディングをすることができます。

https://pico-8.fandom.com/wiki/Lua#Metatables

https://pico-8.fandom.com/wiki/Setmetatable

基底クラス

-- Base Object
objects = {}

Object = {
    x, y, clr
}

function Object:new(o, x, y, clr)
    o = o or {}
    setmetatable(o, { __index = self })
    o.x = x or 0
    o.y = y or 0
    o.clr = clr or colors.white
    return o
end

function Object:update()
end

function Object:draw()
end

-- Stage Object
StageObject = Object:new()

function StageObject:new(o, x, y, w, h, clr)
    o = Object:new(o, x, y, clr)
    setmetatable(o, { __index = self })
    o.w = w or 1
    o.h = h or 1
    return o
end

function StageObject:on_hit(ball)
	sfx(0)
	
	...
end

  1. クラスを定義
  2. コンストラクタを定義
  3. setmetatableで自身を継承させる

1. クラスを定義

  • クラス名 = {}で定義
  • 使用するプロパティを定義

(VSCodeで編集したので、先頭大文字になっていますが、PICO-8の場合すべて小文字にするべきだそうです。c_objectのような命名が望ましいかもしれません。)

Object = {
    x, y, clr
}

2. コンストラクタを定義

  • クラス名:new()でコンストラクタの定義
  • oなどの変数で、オブジェクトを参照する
    • o = o or {}:引数から受け取ったオブジェクトがnilだった場合に、テーブルを新規作成
  • oに値を設定、oを返す
function Object:new(o, x, y, clr)
    o = o or {}
    setmetatable(o, { __index = self })
    o.x = x or 0
    o.y = y or 0
    o.clr = clr or colors.white
    return o
end

3. 継承させる(setmetatable)

  • setmetatable(o, {__index = self})で、__indexに自身を参照させることで自身の情報を継承させる
setmetatable(o, { __index = self })

仕組み(AI回答)

  1. obj.some_method()の呼び出し
  2. objsome_method()が存在しない場合、objのメタテーブル__indexを探す
  3. __indexObjectを指しているので、Objectに定義されたsome_method()を探し、実行

派生クラス

-- block
Block = StageObject:new()

function Block:new(o, x, y, w, h, clr)
    o = StageObject:new(o, x, y, w, h, clr)
    setmetatable(o, { __index = self })
    return o
end

function Block:on_hit(ball)
    -- override
    StageObject.on_hit(self, ball)

    sfx(1)

    -- speed up ball
    ball:speedup()

    -- remove block
    del(objects, self)
    del(blocks, self)

    -- check clear
    check_clear()
end

1. クラスの継承

Block = StageObject:new()

2. コンストラクタの定義

  • インスタンス時、oに対してオブジェクトを指定し、親クラスのnew()に指定する
  • setmetatable()で継承
    • 派生クラス側にもこれがないと、正しくオーバーライドされない
function Block:new(o, x, y, w, h, clr)
    o = StageObject:new(o, x, y, w, h, clr)
    setmetatable(o, { __index = self })
    return o
end

3. メソッドのオーバーライド

  • 親クラスに定義されたメソッドと同名のメソッドを定義する
  • 親クラスのメソッドを呼び出すことで、オーバーライド可能
function Block:on_hit(ball)
    -- override
    StageObject.on_hit(self, ball)

    sfx(1)

インスタンス

function stage_init()
    blocks = {}
    local block_count_x = 6
    local block_count_y = 4

    -- generate blocks
    for by = 0, block_count_y - 1 do
        for bx = 0, block_count_x - 1 do
            local block_w = 16
            local block_h = 6
            local spacing = 2
            local offset_x = 10
            local offset_y = 8

            local block = Block:new(
                self,
                offset_x + bx * (block_w + spacing),
                offset_y + by * (block_h + spacing),
                block_w,
                block_h,
                colors.peach
            )

            add(blocks, block)
            add(objects, block)
        end
    end
end

1. インスタンス化

  • クラス名:new()でインスタンス化
local block = Block:new(
	self,
	offset_x + bx * (block_w + spacing),
	offset_y + by * (block_h + spacing),
	block_w,
	block_h,
	colors.peach
)

また、基底クラスの配列を作成して、_update()などで対象オブジェクトをまとめて呼び出して更新したりできます。

function _update()
    if current_game_state == game_states.playing then
        -- update objects
        for obj in all(objects) do
            obj:update()
        end
    end

メタメソッド (メソッドのオーバーライド)

setmetatableで継承を実装することができますが、特定のテーブルのto_string()、演算子のメソッドなどをオーバーライドすることが可能です。

  1. オーバーライドする関数を格納するテーブルを定義
  2. __xxx = function()でオーバーライド
  3. setmetatable()に対象オブジェクトと、定義したテーブルを指定
  4. オーバーライド対象メソッドに、対象オブジェクトを指定して呼び出すことで、オーバーライドした内容が反映されて呼び出される
local obj = {}
local mt = {
  __tostring = function(t)
    return "This is a custom object."
  end
}
setmetatable(obj, mt)
print(tostr(obj))  -- -> This is a custom object.

テーブル

先述の通り、メタテーブルはトークンを消費したり、ややクセがありますが、
テーブルを使用することで簡易的にオブジェクト指向を利用することができます。

ball = {
    x = 64,
    y = 24,
    size = 2,
    xdir = 1,
    ydir = 1,

    init = function(x, y, size, xdir, ydir)
        self.x = x
        self.y = y
        self.size = size
        self.xdir = xdir
        self.ydir = ydir
    end,
}

さいごに

制約上、ゴリゴリにオブジェクト指向を取り入れることは推奨されないかもしれません。
テーブルによる簡易的なオブジェクトの定義が手っ取り早いかも。

Discussion