2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

初心者がUE4でルービックキューブ風なものを作って強化学習用のライブラリにしたい #5

Last updated at Posted at 2019-11-22

前回に引き続き初心者がUE4を絡めたPythonライブラリを作れないか色々進めていく記事です(ほぼ自分のための備忘録としての面が強い・・)。

最初 : #1
前回 : #4

アニメーション付きの回転の処理をPythonと繋いでいく

前の記事で即時の回転の関数をPythonと繋いでいきましたが、次はアニメーション付きのものも繋いでいきます。

まずはブループリント側の作業から進めます。
大体は即時の回転のBPと同じ感じです。ほぼほぼ必要なものは用意されているので、キューブに対してループを回して、対象の回転対象のキューブに対して回転のフラグを立てるだけです。

image.png

ループを回し、

image.png

回転対象かどうかチェックし、

image.png

フラグを立てます。

一通りの方向の回転のブループリントの関数を用意し、Pythonからも暫定のコードで呼び出してみます。

Content\Scripts\action.py
...
class Action:

    total_delta_time = 0
    rotation_count = 0

    def tick(self, delta_time):
        """
        ゲームPlay中、約1フレームごとに実行される関数。

        Parameters
        ----------
        delta_time : float
            前回のtick呼び出し後からの経過秒。
        """
        self.total_delta_time += delta_time
        if self.total_delta_time > 3 and self.rotation_count == 0:
            self.uobject.rotateXLeft1()
            self.rotation_count += 1
            return
        if self.total_delta_time > 6 and self.rotation_count == 1:
            self.uobject.rotateXLeft2()
            self.rotation_count += 1
            return
        if self.total_delta_time > 9 and self.rotation_count == 2:
            self.uobject.rotateXLeft3()
            self.rotation_count += 1
            return
        if self.total_delta_time > 12 and self.rotation_count == 3:
            self.uobject.rotateXRight1()
            self.rotation_count += 1
            return
...

20191114_1.gif

一通り回転を確認してみて問題なさそうなので次に進みます。
なお、回転対象によっては表示が崩れるというか、荒れる印象なのですが、これは配布用のパッケージングすると直ったりするのでしょうか・・?それとも何か設定が足りない・・・?(アンチエイリアス的な)

外部のPythonからのデータの指定の対応を進めていく

実際に使う場合にはPyPI(pip)などでライブラリ登録して、Jupyterなどからそれらのライブラリを呼び出してUE4側にアクションの指示を出す必要があります。

そこで、ライブラリのPython側からデータが書き込まれる想定のテーブルを用意していきます。
PyActorを経由するモジュールで進めようと思いましたが、不便なところがでてきたので、common以下の汎用モジュールで書いていくことにします。

Win64\common\sqlite_utils.py
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String
...
declarative_meta = declarative_base()
...

# アクションの処理対象に追加された状態のステータス。
ACTION_INSTRUCTION_STATUS_QUEUED = 1

# アクションの実行中(アニメーション中)の状態のステータス。
ACTION_INSTRUCTION_STATUS_RUNNING = 2

# アクションの処理完了済みの状態のステータス。
ACTION_INSTRUCTION_STATUS_ENDED = 3


class ActionInstruction(declarative_meta):
    """
    Pythonライブラリ側からのアクションの指示のデータを扱う
    テーブルのモデル。

    Attributes
    ----------
    id : Column of Integer
        主キーのカラム。
    action : Column of Integer
        指定されたアクション。定義はaction.py内の値に準じる。
    current_status : Column of Integer
        現在のアクションのステータス。このモジュール内の
        ACTION_INSTRUCTION_STATUS_ のプリフィクスを持つ定数の
        定義に準じる。
    skip_animation : Column of Integer
        アニメーションをスキップするかどうかの指定。
        0=スキップしない、1=スキップする形で設定される。
    """

    id = Column(Integer, primary_key=True)
    action = Column(Integer)
    current_status = Column(Integer)
    skip_animation = Column(Integer)
    __tablename__ = 'action_instruction'


session = None


def create_from_python_db_session():
    """
    Pythonライブラリから書き込まれたSQLiteの読み取り用のセッションを
    生成する。

    Returns
    -------
    session : Session
        生成されたSQLiteのセッション。

    Notes
    -----
    既にセッションが生成済みの場合は生成済みのインスタンスが返却される。
    """
    global session
    if session is not None:
        return session
    session_start_time_str = file_helper.get_session_start_time_str()
    file_name = 'from_python_%s.sqlite' % session_start_time_str
    session = create_session(
        sqlite_file_name=file_name,
        declarative_meta=declarative_meta)
    return session

とりあえずセッションが作れて、1行InsertとDeleteが通ることを確認するためのテストを追加しておきます。

Win64\common\tests\test_sqlite_utils.py
def test_create_from_python_db_session():
    session = sqlite_utils.create_from_python_db_session()
    action_instruction = sqlite_utils.ActionInstruction()
    action_instruction.action = 1
    action_instruction.current_status = \
        sqlite_utils.ACTION_INSTRUCTION_STATUS_QUEUED
    action_instruction.skip_animation = 0
    session.add(instance=action_instruction)
    session.commit()

    action_instruction = session.query(
        sqlite_utils.ActionInstruction
    ).filter_by(
        action=1,
        current_status=sqlite_utils.ACTION_INSTRUCTION_STATUS_QUEUED,
        skip_animation=0).one()
    session.delete(action_instruction)
    session.commit()

続いてaction.py側に、用意したテーブルの値を定期的にチェックする処理を書いていきます。
その前に、tickで処理するとかなり頻繁に処理が実行されて無駄(DBアクセスなどが頻繁に実行される)なので、実行頻度を下げておきます。
一旦0.2秒くらいで設定してみます(様子を見て調整)。

BP_Actionを開いて、Tick Interval (secs)を調整することで対応できるようです。デフォルトでは0で、0の場合はフレームレートに応じた実行頻度となります。

image.png

Python側でコンソール出力の指定をしてみて、一応確認します。

Content\Scripts\action.py
class Action:

...

    def tick(self, delta_time):
        """
        ゲームPlay中、一定時間ごとに実行される関数。

        Parameters
        ----------
        delta_time : float
            前回のtick呼び出し後からの経過秒。
        """
        ue.log(delta_time)

image.png

0.2秒ごとに実行されています。大丈夫そうですね。

一定時間ごとにSQLiteのデータを見にいって、実行可能なアクションのデータを存在すれば1件分取得する処理を追加します。
別のアクションで回転中であったり、指定されているアクションのデータが無いタイミングではNoneが返るようにしておきます。

Content\Scripts\action.py
class Action:

    def begin_play(self):
        """ゲーム開始時に実行される関数。
        """
        self.from_python_db_session = \
            sqlite_utils.create_from_python_db_session()

        python_test_runner.run_pyactor_instance_tests(
            pyactor_class_instance=self)

    def tick(self, delta_time):
        """
        ゲームPlay中、一定時間ごとに実行される関数。

        Parameters
        ----------
        delta_time : float
            前回のtick呼び出し後からの経過秒。
        """
        action, skip_animation = self._get_action_instruction_data()
        if action is None:
            return
        pass

    def _get_action_instruction_data(self):
        """
        設定されたアクション情報の取得を行う。次のアクションが実行できる状態
        (回転アニメーションが終了している等)で、且つ次のアクションの指定が
        存在する場合に、未処理の先頭のアクションの値が返却される。

        Returns
        ----------
        action : int or None
            取得されたアクションの種別値。このモジュール内の ACTION_
            のプリフィクスを持つ値が設定される。対象が存在しない場合や
            次のアクションが実行できない条件の場合にはNoneが設定される。
        skip_animation : bool or None
            アニメーションをスキップするかどうかの設定。対象が存在しない
            場合や次のアクションが実行できない条件の場合にはNoneが設定される。
        """
        if self.is_any_cube_rotating():
            return None, None
        query_result = self.from_python_db_session.query(
            ActionInstruction
        ).filter_by(
            current_status=sqlite_utils.ACTION_INSTRUCTION_STATUS_QUEUED
        ).order_by(
            ActionInstruction.id.asc()
        ).limit(1)
        for action_instruction in query_result:
            action = int(action_instruction.action)
            skip_animation = int(action_instruction.skip_animation)
            if skip_animation == 0:
                skip_animation = False
            else:
                skip_animation = True
            return action, skip_animation
        return None, None

    def is_any_cube_rotating(self):
        """
        いずれかのキューブが回転中かどうかの真偽値を取得する。

        Returns
        ----------
        is_rotating : bool
            いずれかのキューブが回転中であればTrueが設定される。
        """
        is_rotating = self.uobject.isAnyCubeRotating()[0]
        return is_rotating

    def test__get_action_instruction_data(self):

        # 指定されているアクションが存在し、且つアニメーションを
        # スキップしない指定の場合の挙動を確認する。
        action_instruction = sqlite_utils.ActionInstruction()
        action_instruction.action = ACTION_ROTATE_X_LEFT_2
        action_instruction.current_status = \
            sqlite_utils.ACTION_INSTRUCTION_STATUS_QUEUED
        action_instruction.skip_animation = 0
        self.from_python_db_session.add(instance=action_instruction)
        self.from_python_db_session.commit()
        action, skip_animation = self._get_action_instruction_data()
        assert_equal(
            action,
            ACTION_ROTATE_X_LEFT_2)
        assert_false(skip_animation)
        self.from_python_db_session.delete(action_instruction)
        self.from_python_db_session.commit()

        # アニメーションをスキップする指定の場合の挙動を確認する。
        action_instruction = sqlite_utils.ActionInstruction()
        action_instruction.action = ACTION_ROTATE_X_LEFT_2
        action_instruction.current_status = \
            sqlite_utils.ACTION_INSTRUCTION_STATUS_QUEUED
        action_instruction.skip_animation = 1
        self.from_python_db_session.add(instance=action_instruction)
        self.from_python_db_session.commit()
        _, skip_animation = self._get_action_instruction_data()
        assert_true(skip_animation)
        self.from_python_db_session.delete(action_instruction)
        self.from_python_db_session.commit()

        # 指定されているデータが存在しない場合の挙動を確認する。
        action, skip_animation = self._get_action_instruction_data()
        assert_equal(action, None)
        assert_equal(skip_animation, None)

    def test_is_any_cube_rotating(self):
        assert_false(
            self.is_any_cube_rotating())

PyActorで指定されたクラスのテストの実行用に、別途対象クラスのテスト用のメソッドを一通り実行するための関数を汎用モジュールの方に追加しておき、begin_playのタイミングで実行します(本当は通常のpytestなどと同様に自動でテスト対象の関数が流れるといいのですが、UE4からクラスがインスタンス化される都合このようにします)。
UE4と繋いでいる都合、すべてのテストパターンは書くのが難しいのでさくっと書ける範囲でテストも書いておきます。

Win64\common\python_test_runner.py
...
def run_pyactor_instance_tests(pyactor_class_instance):
    """
    PyActorで指定されたPythonクラスのインスタンスに設定されている
    一通りのテストの関数の実行を行う。

    Parameters
    ----------
    pyactor_class_instance : *
        対象のPyActorで指定されたPythonクラスのインスタンス。
    """
    print('%sのテストを開始...' % type(pyactor_class_instance))
    is_packaged_for_distribution = \
        file_helper.get_packaged_for_distribution_bool()
    if is_packaged_for_distribution:
        return
    members = inspect.getmembers(pyactor_class_instance)
    for member_name, member_val in members:
        if not inspect.ismethod(member_val):
            continue
        if not member_name.startswith('test_'):
            continue
        print('%s 対象の関数 : %s' % (datetime.now(), member_name))
        pre_dt = datetime.now()
        member_val()
        timedelta = datetime.now() - pre_dt
        print('%s ok. %s秒' % (datetime.now(), timedelta.total_seconds()))

アクションの指定は取れるようになったので、取得したアクションの値に紐づいたブループリント側の関数を呼び出すところを書いていきます。

Content\Scripts\action.py
...
ACTION_KEY_FUNC_NAME_DICT = {
    ACTION_ROTATE_X_LEFT_1: 'rotateXLeft1',
    ACTION_ROTATE_X_LEFT_2: 'rotateXLeft2',
    ACTION_ROTATE_X_LEFT_3: 'rotateXLeft3',
    ACTION_ROTATE_X_RIGHT_1: 'rotateXRight1',
    ACTION_ROTATE_X_RIGHT_2: 'rotateXRight2',
    ACTION_ROTATE_X_RIGHT_3: 'rotateXRight3',
    ACTION_ROTATE_Y_UP_1: 'rotateYUp1',
    ACTION_ROTATE_Y_UP_2: 'rotateYUp2',
    ACTION_ROTATE_Y_UP_3: 'rotateYUp3',
    ACTION_ROTATE_Y_DOWN_1: 'rotateYDown1',
    ACTION_ROTATE_Y_DOWN_2: 'rotateYDown2',
    ACTION_ROTATE_Y_DOWN_3: 'rotateYDown3',
    ACTION_ROTATE_Z_UP_1: 'rotateZUp1',
    ACTION_ROTATE_Z_UP_2: 'rotateZUp2',
    ACTION_ROTATE_Z_UP_3: 'rotateZUp3',
    ACTION_ROTATE_Z_DOWN_1: 'rotateZDown1',
    ACTION_ROTATE_Z_DOWN_2: 'rotateZDown2',
    ACTION_ROTATE_Z_DOWN_3: 'rotateZDown3',
}
...
class Action:

...

    def tick(self, delta_time):
        """
        ゲームPlay中、一定時間ごとに実行される関数。

        Parameters
        ----------
        delta_time : float
            前回のtick呼び出し後からの経過秒。
        """
        action, skip_animation = self._get_action_instruction_data()
        if action is None:
            return
        action_func_name: str = self._get_action_func_name(
            action=action,
            skip_animation=skip_animation,
        )
        action_func = getattr(self.uobject, action_func_name)
        action_func()

    def _get_action_func_name(self, action, skip_animation):
        """
        指定されたアクションの種別値などから、対象のブループリントの
        関数名を取得する。

        Parameters
        ----------
        action : int
            対象のアクションの種別値。
        skip_animation : bool
            アニメーションをスキップするかどうか。

        Returns
        ----------
        func_name : str
            算出された関数名。
        """
        func_name: str = ACTION_KEY_FUNC_NAME_DICT[action]
        if skip_animation:
            last_char = func_name[-1]
            func_name = func_name[0:-1]
            func_name += 'Immediately%s' % last_char
        return func_name
...
    def test__get_action_func_name(self):
        func_name = self._get_action_func_name(
            action=ACTION_ROTATE_X_LEFT_2,
            skip_animation=False)
        assert_equal(func_name, 'rotateXLeft2')

        func_name = self._get_action_func_name(
            action=ACTION_ROTATE_X_LEFT_2,
            skip_animation=True)
        assert_equal(func_name, 'rotateXLeftImmediately2')

アクションの種別値に応じた関数名を持った辞書を用意して、そちらを参照して関数名を取得しています。アニメーションをスキップする設定になっている場合は即時の回転用の関数名のサフィックスを追加する形で対応しています。

これで問題がなければ、SQLiteのテーブルにデータを書き込むことでUE4側で回転するはずです。
手動でSQLiteにレコードを追加して試してみます。

image.png

20191116_1.gif

想定した通りにSQLiteで指定した通りに回転してくれているようです。他のアクションの種別値も設定してみて、回転を確認してみましたがそちらも問題なさそうです。

SQLiteのファイル名などを調整しておく

UE4のPythonとライブラリでのPythonで書き込み方向などでファイルを分けるか・・・と考えていたのですが、相互に書き込んだりする必要が出てきて、無駄だな・・・と感じ始めたため、ファイルは一つにまとめるようにし、ファイル名や変数名なども調整しておきます(詳細は割愛)。

回転状態のステータスを更新するようにする

現状、SQLiteのcurrent_statusの値を更新していないので、回転を繰り返してしまっているので、その辺りを調整します。

主に以下の三つの関数の処理を追加します。

  • tick内で、アニメーションが終了している場合に、アクションの指定でアニメーション中になっているものがあれば終了のステータスを設定する。
  • tick内で、処理すべきアクションの指定があり、且つアニメーションをする設定の場合はアニメーション中のステータスを設定する。
  • tick内で、処理すべきアクションの指定があり、且つアニメーションをスキップする設定の場合は即時て終了のステータスを設定する。
Content\Scripts\action.py
...
    def tick(self, delta_time):
        """
        ゲームPlay中、一定時間ごとに実行される関数。

        Parameters
        ----------
        delta_time : float
            前回のtick呼び出し後からの経過秒。
        """
        self._set_ended_status_to_animation_ended_action()
        action_instruction_id, action, skip_animation = \
            self._get_action_instruction_data()
        if action_instruction_id is None:
            return
        action_func_name: str = self._get_action_func_name(
            action=action,
            skip_animation=skip_animation,
        )
        action_func = getattr(self.uobject, action_func_name)
        action_func()
        self._set_running_status(
            action_instruction_id=action_instruction_id,
            skip_animation=skip_animation)
        self._set_ended_status_if_animation_skipped(
            action_instruction_id=action_instruction_id,
            skip_animation=skip_animation)

    def _set_ended_status_to_animation_ended_action(self):
        """
        アニメーションが完了している場合に、アニメーション中の
        設定になっているアクションの設定のステータスに完了済み
        (ACTION_INSTRUCTION_STATUS_ENDED)を設定する。

        Returns
        ----------
        updated : bool
            更新処理が実行されたかどうか。
        """
        if self.is_any_cube_rotating():
            return False
        target_data_exists = False
        action_instructions = self.action_data_db_session.query(
            sqlite_utils.ActionInstruction
        ).filter_by(
            current_status=sqlite_utils.ACTION_INSTRUCTION_STATUS_RUNNING,
        )
        action_instruction: sqlite_utils.ActionInstruction
        for action_instruction in action_instructions:
            target_data_exists = True
            action_instruction.current_status = \
                sqlite_utils.ACTION_INSTRUCTION_STATUS_ENDED
        if target_data_exists:
            self.action_data_db_session.commit()
            return True
        return False

    def _set_ended_status_if_animation_skipped(
            self, action_instruction_id, skip_animation):
        """
        アニメーションをスキップする設定の場合に、対象のアクション設定
        のステータスに完了済み(ACTION_INSTRUCTION_STATUS_ENDED)を
        設定する。

        Parameters
        ----------
        action_instruction_id : int
            対象のアクション設定の主キーのID。
        skip_animation : bool
            アニメーションをスキップするかどうか。

        Returns
        ----------
        updated : bool
            更新処理が実行されたかどうか。
        """
        if not skip_animation:
            return False
        action_instruction: sqlite_utils.ActionInstruction
        action_instruction = self.action_data_db_session.query(
            sqlite_utils.ActionInstruction
        ).filter_by(
            id=action_instruction_id
        ).one()
        action_instruction.current_status = \
            sqlite_utils.ACTION_INSTRUCTION_STATUS_ENDED
        self.action_data_db_session.commit()
        return True

    def _set_running_status(self, action_instruction_id, skip_animation):
        """
        もしアニメーションする回転設定の場合に、対象のアクション設定
        のステータスに実行中(ACTION_INSTRUCTION_STATUS_RUNNING)を
        設定する。

        Parameters
        ----------
        action_instruction_id : int
            対象のアクション設定の主キーのID。
        skip_animation : bool
            アニメーションをスキップするかどうか。

        Returns
        ----------
        updated : bool
            更新処理が実行されたかどうか。
        """
        if skip_animation:
            return False
        action_instruction: sqlite_utils.ActionInstruction
        action_instruction = self.action_data_db_session.query(
            sqlite_utils.ActionInstruction
        ).filter_by(
            id=action_instruction_id).one()
        action_instruction.current_status = \
            sqlite_utils.ACTION_INSTRUCTION_STATUS_RUNNING
        self.action_data_db_session.commit()
        return True

実際にSQLiteのテーブルの値を入れてみて、1度だけ回転することを確認しました。
また、ステータスの値がアニメーション中に2(アニメーション中)、終わったら3(終了)になっていることや、即時の回転処理などに対してもチェックしておきます。

image.png

これでアニメーションが止まらない・・・ということが無くなりました。

Observationの値の保存をしていく

強化学習入門#1 基本的な用語とGym、PyTorch入門の「Observation」の節の値を作っていきます。学習用の観測値ですね。

やることとしては、

  • 新しくObservation用のSQLiteのテーブルを追加する
  • アクションで終了したタイミングでObservationの値を保存

といった具合の処理を追加していきます。

まずはモデルの追加から。

Win64\common\sqlite_utils.py
class Observation(declarative_meta):
    """
    アクション結果の観測値のデータを扱うテーブルのモデル。
    アクションが終了(アニメーションが終了)する度に値が保存される。

    Attributes
    ----------
    id : Column of Integer
        主キーのカラム。
    action_instruction_id : int
        対象のアクション情報(完了したアクション)の主キーのID。
    position_num : int
        キューブ位置の番号。各面ごとに、順番に設定される。
    color_type : int
        対象の位置の色の種別値。
    """
    id = Column(Integer, primary_key=True)
    action_instruction_id = Column(Integer, index=True)
    position_num = Column(Integer)
    color_type = Column(Integer)
    __tablename__ = 'observation'

※なお、割愛しましたが、SQLAlchemyでのインデックス設定の方法を知ったので、今回のObservation及びActionInstructionの方に、必要そうなカラムに一部index=Trueの指定を追加しています。

続いて、位置の種別値と色の種類の定義を追加していきます。正面をオレンジとし、世界標準の配色設定の状態で後述する記述とスクショのように割り振っていきます。
なお、配色はWikipediaにある世界標準配色と合わせてあります。

image.png

ルービックキューブ - Wikipediaより画像は引用。

位置の種別値を定義

オレンジ面(前面)

以下のように1~9で割り振ります。定数名では「FRONT」という単語を使っていきます。

temp1119_1.png

白面(左面)

以下のように10~18で割り振ります。定数名では「LEFT」という単語を使っていきます。

temp1119_2.png

黄色面(右面)

以下のように19~27で割り振ります。定数名では「RIGHT」という単語を使っていきます。

temp1119_3.png

緑面(上面)

以下のように28~36で割り振ります。定数名では「TOP」という単語を使っていきます。

temp1119_4.png

赤面(背面)

以下のように37~45で割り振ります。定数名では「BACK」という単語を使っていきます。

temp1120_1.png

青面(底面)

以下のように46~54で割り振ります。定数名では「BOTTOM」という単語を使っていきます。

temp1120_2.png

位置の定数を追加しておく

Pythonで定義しておきます。定数名の番号は行と列番号で設定しています。

Win64\common\sqlite_utils.py
POSITION_NUM_FRONT_1_1 = 1
POSITION_NUM_FRONT_2_1 = 2
POSITION_NUM_FRONT_3_1 = 3
POSITION_NUM_FRONT_1_2 = 4
POSITION_NUM_FRONT_2_2 = 5
POSITION_NUM_FRONT_3_2 = 6
POSITION_NUM_FRONT_1_3 = 7
POSITION_NUM_FRONT_2_3 = 8
POSITION_NUM_FRONT_3_3 = 9

POSITION_NUM_FRONT_LIST = [
    POSITION_NUM_FRONT_1_1,
    POSITION_NUM_FRONT_2_1,
    POSITION_NUM_FRONT_3_1,
    POSITION_NUM_FRONT_1_2,
    POSITION_NUM_FRONT_2_2,
    POSITION_NUM_FRONT_3_2,
    POSITION_NUM_FRONT_1_3,
    POSITION_NUM_FRONT_2_3,
    POSITION_NUM_FRONT_3_3,
]

POSITION_NUM_LEFT_1_1 = 10
POSITION_NUM_LEFT_2_1 = 11
POSITION_NUM_LEFT_3_1 = 12
...
POSITION_NUM_LIST = [
    POSITION_NUM_FRONT_1_1,
    POSITION_NUM_FRONT_2_1,
    POSITION_NUM_FRONT_3_1,
    POSITION_NUM_FRONT_1_2,
    POSITION_NUM_FRONT_2_2,
    POSITION_NUM_FRONT_3_2,
    POSITION_NUM_FRONT_1_3,
    POSITION_NUM_FRONT_2_3,
    POSITION_NUM_FRONT_3_3,
    POSITION_NUM_LEFT_1_1,
    POSITION_NUM_LEFT_2_1,
    POSITION_NUM_LEFT_3_1,
    POSITION_NUM_LEFT_1_2,
    ...
    POSITION_NUM_BOTTOM_1_3,
    POSITION_NUM_BOTTOM_2_3,
    POSITION_NUM_BOTTOM_3_3,
]

色の種別値を定義する

こちらはシンプルに6色で定義するだけです。

Win64\common\sqlite_utils.py
COLOR_TYPE_ORANGE = 1
COLOR_TYPE_WHITE = 2
COLOR_TYPE_YELLOW = 3
COLOR_TYPE_GREEN = 4
COLORR_TYPE_BLUE = 5
COLOR_TYPE_RED = 6

COLOR_TYPE_LIST = [
    COLOR_TYPE_ORANGE,
    COLOR_TYPE_WHITE,
    COLOR_TYPE_YELLOW,
    COLOR_TYPE_GREEN,
    COLORR_TYPE_BLUE,
    COLOR_TYPE_RED,
]

resetの処理をPython側に繋ぎこむ

少し前までresetの処理は直接レベルのブループリント上で実行していましたが、そちらをPythonと繋いでいきます。
以前の他の関数同様、レベルのブループリントに各関数を定義してしまっているので、レベルやBP_Actionなどからでも参照できるように、関数ライブラリを追加してそちらに移していきます。
(今度からはちゃんと最初の方から関数ライブラリで対応しよう・・・(自戒))

image.png

処理としてはほぼレベルのブループリントにあった内容そのままで、キューブの取得処理回りなど一部だけ調整してあります(Get All Actors of Classノードに差し替えたりなど、そのまま持ってくれない部分だけ調整)。

一応一回実行してみて、シャッフルされることを確認しておきます(この時点では関数ライブラリの関数をレベルのBPから実行しています)。

image.png

各BP上のassertの処理やPython上のテストも引っかかっている箇所は無さそうなので、大丈夫そうです。

BP_ActionにPythonから呼び出すためのインターフェイスを追加しておきます。

image.png

関数ライブラリの関数を呼び出すだけのシンプルな関数です。
Pythonも調整していきます。アクションの定義を追加します。

Content\Scripts\action.py
...
ACTION_ROTATE_Z_DOWN_2 = 17
ACTION_ROTATE_Z_DOWN_3 = 18
ACTION_RESET = 19
...
ACTION_LIST = [
    ...
    ACTION_ROTATE_Z_DOWN_2,
    ACTION_ROTATE_Z_DOWN_3,
    ACTION_RESET,
]
...
ACTION_KEY_FUNC_NAME_DICT = {
    ...
    ACTION_ROTATE_Z_DOWN_2: 'rotateZDown2',
    ACTION_ROTATE_Z_DOWN_3: 'rotateZDown3',
    ACTION_RESET: 'reset',
}

SQLiteで直接19のアクションのデータを入れてみて、動作を確認してみます。

image.png

image.png

キューブがちゃんとシャッフルされました。
SQLite側のデータを表示を更新して確認してみても、アクションが完了済みのステータスになっています。

image.png

ここはさくっといきましたね。どんどん次を進めていきます。

各キューブの各面での色の値を取る方法を考える

各キューブがどこに位置しているのかは今までの実装で分かりますが、回転などが絡んでくるのでどの面が見えているのか・・・といったあたりを考える必要があります(この値がObservationで必要になってきます)。

回転量から算出する・・とも考えましたが、ワールド基準で回転させたり何度も何度も回転したりしているので、綺麗な値になってくれないケースが多いです。

image.png

シンプルそうに思えて、なんだか計算が面倒くさそうな気配が結構しています・・うーんどうしましょう・・・
しばらく考えたのですが、キューブの基底クラスの平面のアクター(各色の平面)のワールド基準の位置を取得して、計算できそうな気配があるので、試してみます。

ブループリントの辞書を調べてみる

各位置の種別値によって平面の想定されるXYZのワールド基準の座標が取りたいと思ったものの、そういった辞書というか連想配列的なノードをそういえば使ったことがまだありません。
どんな感じなのか調べていってみます。

C++とブループリントで少し話が変わってくるようですが、ブループリント上ではMapを使えばいいようです。すべての型を放り込めるわけではなさそうですが、恐らく基本的な数値や文字列といった型はサポートしていそうな気配があります。

As far as I know, not every type of variable can be defined as a Map or Set in Blueprints... Maybe there's no such limitation in c++, but I'm not sure.
Dictionary?

多次元の連想配列は・・・多次元配列同様、探した感じなさそう?ですね・・・。

そう考えると、それらの多次元の辞書はPython側で定義しておいて、UE4からは平面の座標値のみ取得して、Python側で計算するのがシンプルそうです。

各位置におけるワールド基準の平面の座標を調べておく

基本的に中央の座標が0、端(見えている色の面)部分の平面であれば151(キューブのサイズが100、中央基準から半径分で50、平面の色部分をキューブよりもわずかに外側になって色が見えるように1加えているので151)といった値が出てくるはずです。キューブの基底クラスのBeginPlayのイベントでコンソール出力して確認していきます。

image.png

上記はオレンジの平面の例ですが、キューブ名とワールド基準の座標(GetWorldLocation)を出力するようにしてあります(確認終わったら削除)。
以下で<行番号>-<列番号>: <出力された座標>の形式でメモしていきます(座標0を基準に作っているので大丈夫だとは思っていましたが、一応事前に回転後も対象の値が変動しないことを確認済みです)。

前面の平面位置

  • 1-1: X=-151.000 Y=-100.000 Z=100.000
  • 2-1: X=-151.000 Y=-100.000 Z=-0.000
  • 3-1: X=-151.000 Y=-100.000 Z=-100.000
  • 1-2: X=-151.000 Y=0.000 Z=100.000
  • 2-2: X=-151.000 Y=0.000 Z=-0.000
  • 3-2: X=-151.000 Y=0.000 Z=-100.000
  • 1-3: X=-151.000 Y=100.000 Z=100.000
  • 2-3: X=-151.000 Y=100.000 Z=-0.000
  • 3-3: X=-151.000 Y=100.000 Z=-100.000

左面の平面位置

  • 1-1: X=100.000 Y=-151.000 Z=100.000
  • 2-1: X=100.000 Y=-151.000 Z=0.000
  • 3-1: X=100.000 Y=-151.000 Z=-100.000
  • 1-2: X=0.000 Y=-151.000 Z=100.000
  • 2-2: X=0.000 Y=-151.000 Z=0.000
  • 3-2: X=0.000 Y=-151.000 Z=-100.000
  • 1-3: X=-100.000 Y=-151.000 Z=100.000
  • 2-3: X=-100.000 Y=-151.000 Z=0.000
  • 3-3: X=-100.000 Y=-151.000 Z=-100.000

右面の平面位置

  • 1-1: X=-100.000 Y=151.000 Z=100.000
  • 2-1: X=-100.000 Y=151.000 Z=-0.000
  • 3-1: X=-100.000 Y=151.000 Z=-100.000
  • 1-2: X=0.000 Y=151.000 Z=100.000
  • 2-2: X=0.000 Y=151.000 Z=-0.000
  • 3-2: X=0.000 Y=151.000 Z=-100.000
  • 1-3: X=100.000 Y=151.000 Z=100.000
  • 2-3: X=100.000 Y=151.000 Z=-0.000
  • 3-3: X=100.000 Y=151.000 Z=-100.000

上面の平面位置

  • 1-1: X=100.000 Y=-100.000 Z=151.000
  • 2-1: X=0.000 Y=-100.000 Z=151.000
  • 3-1: X=-100.000 Y=-100.000 Z=151.000
  • 1-2: X=100.000 Y=0.000 Z=151.000
  • 2-2: X=0.000 Y=0.000 Z=151.000
  • 3-2: X=-100.000 Y=0.000 Z=151.000
  • 1-3: X=100.000 Y=100.000 Z=151.000
  • 2-3: X=0.000 Y=100.000 Z=151.000
  • 3-3: X=-100.000 Y=100.000 Z=151.000

背面の平面位置

  • 1-1: X=151.000 Y=100.000 Z=100.000
  • 2-1: X=151.000 Y=100.000 Z=-0.000
  • 3-1: X=151.000 Y=100.000 Z=-100.000
  • 1-2: X=151.000 Y=0.000 Z=100.000
  • 2-2: X=151.000 Y=0.000 Z=-0.000
  • 3-2: X=151.000 Y=0.000 Z=-100.000
  • 1-3: X=151.000 Y=-100.000 Z=100.000
  • 2-3: X=151.000 Y=-100.000 Z=-0.000
  • 3-3: X=151.000 Y=-100.000 Z=-100.000

底面の平面位置

  • 1-1: X=-100.000 Y=-100.000 Z=-151.000
  • 2-1: X=-0.000 Y=-100.000 Z=-151.000
  • 3-1: X=100.000 Y=-100.000 Z=-151.000
  • 1-2: X=-100.000 Y=0.000 Z=-151.000
  • 2-2: X=-0.000 Y=0.000 Z=-151.000
  • 3-2: X=100.000 Y=0.000 Z=-151.000
  • 1-3: X=-100.000 Y=100.000 Z=-151.000
  • 2-3: X=-0.000 Y=100.000 Z=-151.000
  • 3-3: X=100.000 Y=100.000 Z=-151.000

ブループリント側で色ごとに現在の平面の座標のリストを取得する処理を追加する

Python側で色の位置判定などをする都合、ブループリントで色ごとの平面のワールド基準の座標の配列を取得する処理を追加します。
後でPythonと繋ぎます。

関数の重複部分をなるべく統一するために、対象の平面の指定以外は共通化していきます。
特定のBPクラス内のアクターを取得するにはどうすればいいのだろう・・・と思いましたが、Get All Actors with Tagというノードで取れそうな気配があります。

image.png

タグはどこで設定するのだろう・・・と探してみたら、Detailsウインドウで普通に設定できるようです。

image.png

これらを使って想定したものが作れるか試してみます。
....が、試していたところ、どうやらこれではうまいこと平面のアクターが取れないようです(よく分からず悩むこと10分程度・・・)。
返却値の件数の配列が必ず0になってしまいます。
どうやらGet Components by Tagノードの方が合っているようです。
若干頭が混乱してきましたが、取得対象がアクターではなくコンポーネントだから?ということなんでしょうか?

アクターの定義が、ブループリントクラスとかでレベルに配置されたもの、コンポーネントはそれらのアクター内に格納されるもの、みたいな定義なんでしょうか。後で調べないといまいちすっきりしませんね・・・

image.png

とりあえずGet Components by Tagノードであれば正常に平面が取れたっぽいので気を取り直して進めていきます。

LIB_CubeColorというブループリントの関数ライブラリのファイルを追加して対応していっています。

image.png

引数で取得対象の平面のタグ名を指定してます。
タグは紫色のNameという型が使われるようです。Stringとどう違うのだろう・・。

タグ名の指定を間違えていた時にすぐ気づけるように、関数の最初で想定されるタグ名に引数がなっているかどうかのチェックを挟んでいます。

image.png

↑の配列の値以外ならエラーメッセージを出力しておきます。

image.png

image.png

続いてワールド内の各キューブに対してループを回していっています。
キューブのアクター経由じゃないと、平面取得用のGet Components by Tagノードが呼び出せなかったため、一旦先にキューブアクターを取得しています。
やっぱりアクターの方が親・コンポーネントの方が子という認識で合っているのだろうか・・・

image.png

キューブアクターからGet Components by Tagノードで平面を取得します。
Components Classには、Static Mesh Componentを選択しました(BP上での平面の表示がこれだったので)。

image.png

Is Visibleノードで平面が表示されているかどうかをチェックしています。
キューブの基底クラスには配置自体は一通りの色の平面がされており、各キューブのサブクラスのBPで表示・非表示を設定しているので、非表示になっているものは対象外としています。

image.png

後はGetWorldLocationで平面のワールド基準の座標を取って、返却値用の配列に追加しています。
floatだと微妙に値がずれたりするのと、比較で使うためRoundノードを挟んで整数に変換しています。

ループが終わった後の処理を作っていきます。
一応、結果のデータの件数がちゃんと9件になっているかどうかのチェック用の処理を挟んでおきます。

image.png

image.png

最後に配列を3つ返して終了です。

image.png

試してみます。とりあえずアニメーションする前のオレンジの平面の座標を出力してみます。

  • X: -151, Y: 0, Z: 0
  • X: -151, Y: 0, Z: -100
  • X: -151, Y: 0, Z: 100
  • X: -151, Y: -100, Z: 0
  • X: -151, Y: -100, Z: -100
  • X: -151, Y: -100, Z: 100
  • X: -151, Y: 100, Z: 0
  • X: -151, Y: 100, Z: -100
  • X: -151, Y: 100, Z: 100

少し前に調べた、前面のキューブの座標のリストと組み合わせが一致しています。大丈夫そうですね。

長くなってきたので一旦この辺りで今回の記事を終わりにしたいと思います。
次回はObservation回りの実装をもっといろいろ進めていきます。

参考ページまとめ

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?