RPG風にキャラクタを動かす - Kivy Advent Calendar 2013


Animationオブジェクトの実験ですが、無駄にゲーム機っぽいインタフェースにしてみました。
でもA/B/X/Yキーはダミーです。動くのは上下左右キーだけ。
エラーチェック何もしてませんので、平気で画面外へ出て行っちゃいます...
 
※spellyonさんのサイト「点睛集積」から素材をお借りしています (http://dispell.net/)


(/sdcard/kivy/thrpg/)

android.txt お約束
main.py スクリプト本体
th7_remari.png 霊夢魔理沙の24x32チビキャラファイル(背景を透過化)
chara.atlas 上の画像に紐付けるatlasファイル 霊夢のデータしか使ってません...

(android.txt)

title=thrpg
author=cheeseshop
orientation=landscape

(chara.atlas)

{
    "th7_remari.png": {
        "N0": [ 24, 224, 24, 32],
        "N1": [  0, 224, 24, 32],
        "N2": [ 24, 224, 24, 32],
        "N3": [ 48, 224, 24, 32],
        "E0": [ 24, 192, 24, 32],
        "E1": [  0, 192, 24, 32],
        "E2": [ 24, 192, 24, 32],
        "E3": [ 48, 192, 24, 32],
        "S0": [ 24, 160, 24, 32],
        "S1": [  0, 160, 24, 32],
        "S2": [ 24, 160, 24, 32],
        "S3": [ 48, 160, 24, 32],
        "W0": [ 24, 128, 24, 32],
        "W1": [  0, 128, 24, 32],
        "W2": [ 24, 128, 24, 32],
        "W3": [ 48, 128, 24, 32]
    }
}

(main.py)

from kivy.app import App
from kivy.animation import Animation
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.image import Image
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.relativelayout import RelativeLayout
from itertools import cycle

from kivy.lang import Builder
Builder.load_string('''\
:
    orientation: 'vertical'
    Label:
        size_hint_y: 3./8.
    GridLayout:
        size_hint_y: 2./8.
        rows: 3
        Label:
        Button:
            id: btn_N
            text: '^'
            on_press: app.move('N')
        Label:
        Button:
            id: btn_W
            text: '<'
            on_press: app.move('W')
        Label:
        Button:
            id: btn_E
            text: '>'
            on_press: app.move('E')
        Label:
        Button:
            id: btn_S
            text: 'v'
            on_press: app.move('S')
        Label:
    Label:
        size_hint_y: 3./8.

:
    orientation: 'vertical'
    Label:
        size_hint_y: 3./8.
    GridLayout:
        size_hint_y: 2./8.
        rows: 3
        Label:
        Button:
            text: 'X'
        Label:
        Button:
            text: 'Y'
        Label:
        Button:
            text: 'A'
        Label:
        Button:
            text: 'B'
        Label:
    Label:
        size_hint_y: 3./8.

:
    game: _game
    orientation: 'horizontal'
    ControlPad:
        size_hint_x: 1./8.
    Game:
        id: _game
        size_hint_x: 6./8.
    ActionPad:
        size_hint_x: 1./8.

:
    canvas:
        Color:
            rgb: (.25, .50, .25)
        Rectangle:
            size: self.size
            pos: (0, 0)

:
    drct: 'E'
    source: 'atlas://chara/E0'
    allow_stretch: True
    size: (192, 256)
    size_hint: (None, None)
''')

class Root(BoxLayout):
    pass

class ControlPad(BoxLayout):
    pass

class ActionPad(BoxLayout):
    pass

class Game(RelativeLayout):
    pass

class Character(Image):
    pass

class GameApp(App):

    def cycle(self, iter=cycle(list('0123'))):
        return iter.next()

    def reload(self, anim, ch, progress):
        ch.source = 'atlas://chara/%s%s' % (ch.drct,self.cycle())
        ch.reload()

    def clear(self, anim, ch):
        self.moving = False

    def move(self, drct, *args):
        if self.moving:
            return False
        self.moving = True
        self.anim = Animation(
            d=1./1., s=1./8., t='linear',
            x=self.ch.x+(drct=='E')*192-(drct=='W')*192,
            y=self.ch.y+(drct=='N')*256-(drct=='S')*256)
        self.ch.drct = drct
        self.anim.bind(on_progress=self.reload)
        self.anim.bind(on_complete=self.clear)
        self.anim.start(self.ch)
        return False

    def build(self):
        root = Root()
        self.moving = False
        self.ch = Character()
        self.ch.pos = (0, 0)
        self.ch.drct = 'S'
        root.game.add_widget(self.ch)
        return root

if __name__ == '__main__':
    GameApp().run()

解説

Kivyのアニメーション機能

Animationオブジェクトは、描画タイミングにあわせてウィジェットのサイズや位置といったプロパティを逐次変更していくというものです。
引数には次のようなものを指定してインスタンスを生成します。

  • サイズや位置の最終値 (x, y, pos, size)
  • 描画タイミング (d:アニメーションの時間、s:描画間隔)
  • トランジション関数 (t:文字列または3つの引数[初期値,最終値,0〜1の値]を取る関数)

その後 start(widget) メソッドで、引数に指定したウィジェットの移動を開始します。

歩行アニメーション

Animationオブジェクトは、あくまでウィジェットの「数値の」プロパティを順次変更するものです。
関数をうまく使えば画像を変更するのも可能かもしれませんが、ちょっと面倒なので別の方法を取ることにしました。
このサンプルでは、描画タイミングで「on_progressイベントが発生する」ことを利用しています。on_progressで呼び出すハンドラの第2引数には移動中のウィジェットが入るので、そこで同じ向きのキャラクタ画像 (3パターン) を順次入れ替えて歩行のアニメーションを実現しています。

排他処理

アニメーションはプログラムをブロックせず並行に走るので、動作中に別の移動ボタンを受け付けてしまう可能性があります。このサンプルでは「ひとつの移動が終わってから次のキーを受け付けるようにしたい」ので、移動が終わったときのon_completeイベントとフラグ (moving) を使って排他制御をしています。

キー操作がリピートしない

ゲームキーパッドもどきはon_pressを使っているので、その都度離して押してを繰り返さないといけないです。この件については後で解決策を探してみます...
(たぶんon_touch_downを使えばいいんでしょうが、本当にそのボタンを押したかをcollide_pointでチェックする必要があります)