[PYTHON] Beginners want to make something like a Rubik's cube with UE4 and make it a library for reinforcement learning # 4

Continuing from the last time, this is an article in which beginners will continue to work on creating a Python library involving UE4 (almost as a memorandum for myself ...).

First: # 1 Last time: # 3

Try importing with Python script etc.

I specify one of the Python modules in the PyActor blueprint, but at that time I tried to see if I could import the Python modules under the other Script folder. It feels like I tried to output to the console with ue.log etc., and it seems that only the one specified in the PyActor blueprint is executed. It seems that the import itself of other modules does not cause an error, but when I call a function added in that module, an error occurs. You can use the one installed with pip without any problem, but basically it seems better to think of it as one module for one PyActor.

However, if that is the case, you will be in trouble with common processing. So, try adding a folder such as common to the folder where the library is added with pip and import it.

Library folder is added by pip of Plugins \ UnrealEnginePython \ Binaries \ Win64 A folder called common is added to the plugin directory, and a file called sqlite_utils.py is added assuming that common processing of SQLite related things is written. Try the following description in that file.

sqlite_utils.py


def print_test(ue):
    ue.log('sqlite_utils test')

Next, add the following description in the module specified by PyActor.

import unreal_engine as ue

from common import sqlite_utils

sqlite_utils.print_test(ue=ue)

The directory handled by pip should be in the path, so I'm trying it on the assumption that it should be able to be imported. Let's preview in UE4 (or rather, Play is the correct word ...).

image.png

It seems that the imported module is being called successfully. When you need to write Python that spans multiple modules, it seems good to add the module to the library folder.

Also, instead of directly importing unreal_engine in the common.sqlite_utils module, it is passed as a function argument from the module specified by PyActor, but if this is not the module specified by PyActor, it will also work as ue.log. did not. Perhaps you can't import the unreal_engine module except for the module specified in PyActor. Therefore, if the unreal_engine module is required on the common module side, pass it as an argument this time.

Think about what to do with testing Python scripts

For development, I would like to write a test with a Python script. However, as mentioned above, there is a restriction that there is one module for each PyActor class, and those involving the unreal_engine module cannot be tested without playing with UE4, so a test runner such as pytest cannot be used.

So, although it seems irregular, I will proceed as follows.

--Write the test function with the prefix test_ in the module itself specified by each PyActor. --Prepare a simple code for your own test runner (do not make elaborate ones) in the common module. --PyActor Pass your module as an argument to your own test runner at the top level of the specified module, and execute the prefix function test_ on the passed test runner side.

For the time being, we will start with our own test runner.

py:common.python_test_runner.py


"""A module that handles the test execution of each Python script.

Notes
-----
- unreal_For the convenience of entwining the engine module, without separating the test module
Operate.
"""

from datetime import datetime
import inspect


def run_tests(ue, target_module):
    """
Run the tests defined for the module of interest.

    Parameters
    ----------
    ue : unreal_engine
Imported within the Python module specified by each PyActor,
UnrealEngine Python library module.
    target_module : module
The module to be tested. Test in module_Of the prefix
The function is executed.
    """
    ue.log(
        '%s %Start testing the s module...'
        % (datetime.now(), target_module.__name__))
    members = inspect.getmembers(target_module)
    for obj_name, obj_val in members:
        if not inspect.isfunction(obj_val):
            continue
        if not obj_name.startswith('test_'):
            continue
        ue.log(
            '%s Target function: %s' % (datetime.now(), obj_name))
        pre_dt = datetime.now()
        obj_val()
        timedelta = datetime.now() - pre_dt
        ue.log('%s ok. %s seconds' % (datetime.now(), timedelta.total_seconds()))

I'll adjust the details later, but once it's a simple implementation, it's fine. With the getmembers function of the built-in inspect module, you can take the member element of the module specified as an argument, so turn it in a loop, check if it is a function with the isfunction function, and flow processing only when the function name has a prefix of test_ I am doing it. All that remains is the description that outputs the test time, target module name, function name, etc. to the console.

Separately, in the module specified by PyActor, prepare a module for writing SQLite to pass data in the flow of UE4 → Python library. I prepared an addition function called add for checking the operation of the test runner, which I will erase later.

to_python_sqlite_writer.py


"""Handles writing data from UE4 to SQLite for Python libraries
Module for.
"""

import sys
import importlib
import time
import unittest

import unreal_engine as ue

from common import python_test_runner

importlib.reload(python_test_runner)


def add(a, b):
    time.sleep(1)
    return a + b


def test_add():
    added_val = add(a=1, b=3)
    assert added_val == 4


python_test_runner.run_tests(
    ue=ue, target_module=sys.modules[__name__])

To check the display of the test time, I intentionally put it to sleep for 1 second in the function. As mentioned above, I try to write tests in the same module. Finally, the test runner process is called at the top level. You can get your own module by doing sys.moduels [__name__].

In addition, the module specified by PyActor is automatically reloaded on the UE4 side after updating the code (because the setting pop-up appears), but it seems that the common module etc. are not automatically reloaded. Therefore, the code change may not be reflected immediately, so I reload it with importlib for immediate reflection (maybe it will be deleted in the end ...).

Let's play with UE4.

image.png

The test has passed. I'll adjust the details later if necessary, but for the time being, it seems okay ...

Put nose in the test library

Recently, when writing code privately, I often use pytest because of the excellence of test runners, but this time I only need to use assert functions (assert_equal and assert_raises), so to make things easier, nose test library I will put it in.

$ ./python.exe -m pip install --target . nose
Successfully installed nose-1.3.7

To make sure it works on UE4, I'll adjust the code I wrote in the test validation a while back.

to_python_sqlite_writer.py


from nose.tools import assert_equal
...
def test_add():
    added_val = add(a=1, b=3)
    assert_equal(added_val, 4)

image.png

It seems to work fine.

Let's proceed with the reading and writing part in SQLite

Last time, I installed SQLAlchemy for SQLite and supported it to the point where import etc. can be done at least, but I will confirm that there is no problem with a little more verification and organize the provisional files. First, delete python_and_h5py_test.py used for verification such as h5py and the blueprint class associated with it.

First, add the process to get the character string to specify the path of SQLAlchemy of SQLite in the common module.

common\sqlite_utils.py


"""A module that describes common processing related to SQLite.
"""

import os
import sys

DESKTOP_FOLDER_NAME = 'cubicePuzzle3x3'


def get_sqlite_engine_file_path(file_name):
    """
Get the file path for specifying the engine for SQLAlchemy in SQLite.

    Parameters
    ----------
    file_name : str
The name of the target SQL file that contains the extension.

    Returns
    -------
    sqlite_file_path : str
A string of paths for specifying the engine for SQLite.
        sqlite:///Starting from, a folder for SQLite is created on the desktop
Set in shape.

    Notes
    -----
It is created if the save destination folder does not exist.
    """
    dir_path = os.path.join(
        os.environ['HOMEPATH'], 'Desktop', DESKTOP_FOLDER_NAME)
    os.makedirs(dir_path, exist_ok=True)
    sqlite_file_path = 'sqlite:///{dir_path}/{file_name}'.format(
        dir_path=dir_path,
        file_name=file_name,
    )
    return sqlite_file_path

As I wrote it, I noticed that I can't write a test for a common module with this (this module itself doesn't import the ue module).

Therefore, add a module for executing the test of the common module and a blueprint class of PyActor, and execute the test of each common module via that.

Content\Scripts\run_common_module_tests.py


"""For modules under the common directory of Python plugins
Run the test.
"""

import sys
import inspect

import unreal_engine as ue

from common import python_test_runner
from common.tests import test_sqlite_utils

NOT_TEST_TARGET_MODULES = [
    sys,
    inspect,
    ue,
    python_test_runner,
]

members = inspect.getmembers(sys.modules[__name__])
for obj_name, obj_val in members:
    if not inspect.ismodule(obj_val):
        continue
    is_in = obj_val in NOT_TEST_TARGET_MODULES
    if is_in:
        continue
    python_test_runner.run_tests(ue=ue, target_module=obj_val)

The BP side named it BP_RunCommonModuleTests.

I will write a test. For the common module, importing the ue module is unnecessary due to the above code, and there is no problem even if the modules are separated, so test_ .py.

common\tests\test_sqlite_utils.py


"""sqlite_A module for testing the utils module.
"""

from nose.tools import assert_equal, assert_true

from common import sqlite_utils


def test_get_sqlite_engine_file_path():
    sqlite_file_path = sqlite_utils.get_sqlite_engine_file_path(
        file_name='test_dbfile.sqlite')
    assert_true(
        sqlite_file_path.startswith('sqlite:///')
    )
    is_in = sqlite_utils.DESKTOP_FOLDER_NAME in sqlite_file_path
    assert_true(is_in)
    assert_true(
        sqlite_file_path.endswith('/test_dbfile.sqlite')
    )

Let's play UE4.

image.png

Sounds okay. Now you can write tests on the common module side as well. The problem is that it is not possible to test only specific modules or specific functions, which is prepared by pytest etc. I'll think about this point when the number of tests increases and it feels quite awkward (such as making it possible to quickly test only specific modules). There is a high possibility that the test time will remain at a level that does not bother you until the implementation is completed.

Check the behavior for SQLite test, and consider the initialization process and the test when the class is sandwiched between PyActor scripts.

When I was working on UE4 and tested using SQLite, there was a case where the SQLite file remained locked even if Play was executed again (such as when an error occurred in the middle). ).

When it is actually packaged, it seems that the application will be dropped once, but during development it is troublesome to restart UE4 every time to unlock it. Make sure to insert the date and time information at the time you press Play into the file name so that it will be a different SQLite file each time you play. Add the name initializer.py and the PyActor blueprint.

Content\Scripts\initializer.py


"""A module that describes the process that is executed first, such as when the game starts.
"""

import json
from datetime import datetime
import os
import sys
import time
import importlib

from nose.tools import assert_equal, assert_true
import unreal_engine as ue

from common import const, file_helper
from common.python_test_runner import run_tests
importlib.reload(const)
importlib.reload(file_helper)


def save_session_data_json():
    """
To retain information at the start of a single game session
Save the JSON file.
    """
    session_data_dict = {
        const.SESSION_DATA_KEY_START_DATETIME: str(datetime.now()),
    }
    file_path = file_helper.get_session_json_file_path()
    with open(file_path, mode='w') as f:
        json.dump(session_data_dict, f)
    ue.log('initialized.')


save_session_data_json()


def test_save_session_data_json():
    pre_session_json_file_name = const.SESSION_JSON_FILE_NAME
    const.SESSION_JSON_FILE_NAME = 'test_game_session.json'

    expected_file_path = file_helper.get_session_json_file_path()
    save_session_data_json()
    assert_true(os.path.exists(expected_file_path))
    with open(expected_file_path, 'r') as f:
        json_str = f.read()
    data_dict = json.loads(json_str)
    expected_key_list = [
        const.SESSION_DATA_KEY_START_DATETIME,
    ]
    for key in expected_key_list:
        has_key = key in data_dict
        assert_true(has_key)

    os.remove(expected_file_path)
    const.SESSION_JSON_FILE_NAME = pre_session_json_file_name


run_tests(
    ue=ue,
    target_module=sys.modules[__name__])

We will also add a common module for file operations and its tests.

Win64\common\file_helper.py


"""A module that describes common processing related to file operations.
"""

import os
import time
import json
from datetime import datetime

from common.const import DESKTOP_FOLDER_NAME
from common import const


def get_desktop_data_dir_path():
    """
Get the directory for saving data on the desktop.

    Returns
    -------
    dir_path : str
Directory path for saving the retrieved desktop data.

    Notes
    -----
It is created if the save destination folder does not exist.
    """
    dir_path = os.path.join(
        os.environ['HOMEPATH'], 'Desktop', DESKTOP_FOLDER_NAME)
    os.makedirs(dir_path, exist_ok=True)
    return dir_path


def get_session_json_file_path():
    """
To retain information at the start of a single game session
Get the path of the JSON file.

    Returns
    -------
    file_path : str
Target file path.
    """
    file_path = os.path.join(
        get_desktop_data_dir_path(),
        const.SESSION_JSON_FILE_NAME
    )
    return file_path


def get_session_start_time_str(remove_symbols=True):
    """
Get the character string of the date and time at the start of one game session from the JSON file.
It is used for SQLite file names.

    Parameters
    ----------
    remove_symbols : bool, default True
Remove the symbol from the return value string and change it to a value with only half-width integers.
Whether to convert.

    Returns
    -------
    session_start_time_str : str
A string of dates and times at the start of one game session.
    """
    time.sleep(0.1)
    file_path = get_session_json_file_path()
    with open(file_path, mode='r') as f:
        data_dict = json.load(f)
    session_start_time_str = str(
        data_dict[const.SESSION_DATA_KEY_START_DATETIME])
    if remove_symbols:
        session_start_time_str = session_start_time_str.replace('-', '')
        session_start_time_str = session_start_time_str.replace('.', '')
        session_start_time_str = session_start_time_str.replace(':', '')
        session_start_time_str = session_start_time_str.replace(' ', '')
    return session_start_time_str

Also add a module for SQLite common processing.

Win64\common\sqlite_utils.py


"""A module that describes common processing related to SQLite.
"""

import os
import sys

import sqlalchemy
from sqlalchemy.orm import sessionmaker

from common import file_helper


def get_sqlite_engine_file_path(file_name):
    """
Get the file path for specifying the engine for SQLAlchemy in SQLite.

    Parameters
    ----------
    file_name : str
The name of the target SQL file that contains the extension.

    Returns
    -------
    sqlite_file_path : str
A string of paths for specifying the engine for SQLite.
        sqlite:///Starting from, a folder for SQLite is created on the desktop
Set in shape.

    Notes
    -----
It is created if the save destination folder does not exist.
    """
    dir_path = file_helper.get_desktop_data_dir_path()
    sqlite_file_path = 'sqlite:///{dir_path}/{file_name}'.format(
        dir_path=dir_path,
        file_name=file_name,
    )
    return sqlite_file_path


def create_session(sqlite_file_name, declarative_meta):
    """
Create a SQLite session.

    Parameters
    ----------
    sqlite_file_name : str
The name of the target SQLite file.
    declarative_meta : DeclarativeMeta
An object that stores the metadata for each table in the target SQLite.

    Returns
    -------
    session : Session
Generated SQLite session.
    """
    sqlite_file_path = get_sqlite_engine_file_path(
        file_name=sqlite_file_name)
    engine = sqlalchemy.create_engine(sqlite_file_path, echo=True)
    declarative_meta.metadata.create_all(bind=engine)
    Session = sessionmaker(bind=engine)
    session = Session()
    return session

Win64\common\tests\test_sqlite_utils.py


"""sqlite_A module for testing the utils module.
"""

import os
from nose.tools import assert_equal, assert_true
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer

from common import sqlite_utils, file_helper


def test_get_sqlite_engine_file_path():
    sqlite_file_path = sqlite_utils.get_sqlite_engine_file_path(
        file_name='test_dbfile.sqlite')
    assert_true(
        sqlite_file_path.startswith('sqlite:///')
    )
    is_in = file_helper.DESKTOP_FOLDER_NAME in sqlite_file_path
    assert_true(is_in)
    assert_true(
        sqlite_file_path.endswith('/test_dbfile.sqlite')
    )


def test_create_session():
    if not os.path.exists(file_helper.get_session_json_file_path()):
        return
    session_start_time_str = file_helper.get_session_start_time_str()
    sqlite_file_name = 'test_%s.sqlite' % session_start_time_str
    expected_file_path = sqlite_utils.get_sqlite_engine_file_path(
        file_name=sqlite_file_name)
    if os.path.exists(expected_file_path):
        os.remove(expected_file_path)

    declarative_meta = declarative_base()

    class TestTable(declarative_meta):
        id = Column(Integer, primary_key=True)
        __tablename__ = 'test_table'

    session = sqlite_utils.create_session(
        sqlite_file_name=sqlite_file_name,
        declarative_meta=declarative_meta)

    test_data = TestTable()
    session.add(instance=test_data)
    session.commit()
    query_result = session.query(TestTable)
    for test_data in query_result:
        assert_true(isinstance(test_data.id, int))

    expected_file_path = expected_file_path.replace('sqlite:///', '')
    assert_true(
        os.path.exists(expected_file_path))

    session.close()
    os.remove(expected_file_path)

It's okay to put initializer.py, but when you actually use it, you need to have the module that connects to SQLite executed after initializer. On the other hand, it is unclear which module will be executed first in PyActor (in some cases, top-level scripts of other modules will be executed before initializer).

Therefore, if it is not after initializer, specify the class with PyActor and start it via the begin_play method (via begin_play, it will be executed after the top level processing of each module. Seems like, at least as far as I've tried).

Content\Scripts\to_python_sqlite_writer.py


"""Handles writing data from UE4 to SQLite for Python libraries
Module for.
"""

import sys
import importlib
import time
import unittest
import time

import unreal_engine as ue
from nose.tools import assert_equal, assert_true
import sqlalchemy
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String

from common import python_test_runner, sqlite_utils, file_helper

importlib.reload(python_test_runner)
importlib.reload(sqlite_utils)
declarative_meta = declarative_base()


class ToPythonSqliteWriter:

    class TestTable(declarative_meta):
        id = Column(Integer, primary_key=True)
        name = Column(String(length=256))
        __tablename__ = 'test_table'

    def begin_play(self):
        """
A function that is executed when the game Play starts.

        Notes
        -----
It is executed after the top-level processing of each module.
        """
        self.session_start_time_str = \
            file_helper.get_session_start_time_str()
        self.SQLITE_FILE_NAME = 'to_python_from_ue4_%s.sqlite'\
            % self.session_start_time_str

        self.session = sqlite_utils.create_session(
            sqlite_file_name=self.SQLITE_FILE_NAME,
            declarative_meta=declarative_meta,
        )
        python_test_runner.run_begin_play_test(
            begin_play_test_func=self.test_begin_play
        )

    def test_begin_play(self):
        assert_equal(
            self.session_start_time_str,
            file_helper.get_session_start_time_str(),
        )
        is_in = 'to_python_from_ue4_' in self.SQLITE_FILE_NAME
        assert_true(is_in)
        is_in = '.sqlite' in self.SQLITE_FILE_NAME
        assert_true(is_in)
        query_result = self.session.query(self.TestTable)


python_test_runner.run_tests(
    ue=ue, target_module=sys.modules[__name__])

Also, since the currently added test runner only supports top-level ones, we will insert a function called run_begin_play_test to execute a test for a method related to a special UE4 event of begin_play.

Win64\common\python_test_runner.py


def run_begin_play_test(begin_play_test_func):
    """
Begin of the class specified by PyActor_Test for play method
Execute.

    Parameters
    ----------
    begin_play_test_func : function
Target begin_Test the play method.
    """
    begin_play_test_func()

Currently, there is almost no content, but I will insert a process so that it will be executed only during development later (because it has not been linked with the UE4 side to that extent yet).

Check the generated SQLite file

If you check that the test passes and check the desktop, you can see that the file is generated for the time being.

image.png

Let's check the contents a little. Install DB Browser for SQLite and check the contents.

image.png

For the time being, SQLite via UE4 seems to work fine.

Allow Python to get the value of whether it is a packaged environment

On the Python side as well, make it possible to take the value of the Is Packaged for Distribution node so that tests etc. are executed only during development. However, because the function call can only be done via uobject, it must go through the class, and it cannot be processed at the top level part of initializer. Well, it's just about reducing unnecessary processing, and it's not something that can be done, so let's proceed without worrying about the details ...

Since we need to call the function on the UE4 blueprint from Python, first add the target function to the blueprint.

image.png

It is a function that simply returns the value of the prepared function. Is it possible to call a directly prepared function (green instead of purple) from Python without this function? I tried, but it was played without such a thing, so I will prepare it normally.

Also, specify the class on the blueprint.

image.png

We will proceed with the support on the Python side. We need to go through the uobject of the class, so prepare the class.

Content\Scripts\initializer.py


class Initializer:

    def begin_play(self):
        """
A function that is executed when the game Play starts.
        """
        self.is_packaged_for_distribution = \
            self.uobject.isPackagedForDistribution()[0]
        _update_packaged_for_distribution_value(
            is_packaged_for_distribution=self.is_packaged_for_distribution)


def _update_packaged_for_distribution_value(is_packaged_for_distribution):
    """
Whether the environment is packaged for distribution (for production)
Update the boolean value.

    Parameters
    ----------
    is_packaged_for_distribution : bool
Boolean value to set.
    """
    file_path = file_helper.get_session_json_file_path()
    if os.path.exists(file_path):
        with open(file_path, mode='r') as f:
          session_data_dict = json.load(f)
    else:
        session_data_dict = {}
    if is_packaged_for_distribution:
        is_packaged_for_distribution_int = 1
    else:
        is_packaged_for_distribution_int = 0
    with open(file_path, mode='w') as f:
        session_data_dict[
            const.SESSION_DATA_KEY_IS_PACKAGED_FOR_DISTRIBUTION] = \
            is_packaged_for_distribution_int
        json.dump(session_data_dict, f)


def test__update_packaged_for_distribution_value():
    pre_session_json_file_name = const.SESSION_JSON_FILE_NAME
    const.SESSION_JSON_FILE_NAME = 'test_game_session.json'
    expected_file_path = file_helper.get_session_json_file_path()

    _update_packaged_for_distribution_value(
        is_packaged_for_distribution=True)
    with open(expected_file_path, mode='r') as f:
        session_data_dict = json.load(f)
    assert_equal(
        session_data_dict[
            const.SESSION_DATA_KEY_IS_PACKAGED_FOR_DISTRIBUTION],
        1
    )

    _update_packaged_for_distribution_value(
        is_packaged_for_distribution=False)
    with open(expected_file_path, mode='r') as f:
        session_data_dict = json.load(f)
    assert_equal(
        session_data_dict[
            const.SESSION_DATA_KEY_IS_PACKAGED_FOR_DISTRIBUTION],
        0
    )

    os.remove(expected_file_path)
    const.SESSION_JSON_FILE_NAME = pre_session_json_file_name

In addition, although the value is taken as self.uobject.isPackagedForDistribution () [0], even if only one return value of the Blueprint function is passed as a tuple on the Python side, it is done like this I will. Even if I output to the console on UE4, it is False, but somehow the branch does not work ... It takes a few minutes to worry. Apparently, it is output to the console directly with a feeling like False or True instead of a tuple display like (False,). This is confusing ... (at least a comma ...) When I displayed the type, I noticed that it was a tuple.

I will also add the value acquisition process.

Win64\common\file_helper.py


def get_packaged_for_distribution_bool():
    """
Gets the boolean value of whether or not it is in a packaged state for distribution.

    Notes
    -----
Only at the beginning of the first startup, the timing when the value cannot be obtained normally before saving the value
Exists. In that case, False will be returned.

    Returns
    -------
    is_packaged_for_distribution : bool
True and packaged for distribution (for production), False
For development.
    """
    file_path = get_session_json_file_path()
    if not os.path.exists(file_path):
        return False
    with open(file_path, mode='r') as f:
        json_str = f.read()
        if json_str == '':
            session_data_dict = {}
        else:
            session_data_dict = json.loads(json_str)
    has_key = const.SESSION_DATA_KEY_IS_PACKAGED_FOR_DISTRIBUTION \
        in session_data_dict
    if not has_key:
        return False
    is_packaged_for_distribution = session_data_dict[
        const.SESSION_DATA_KEY_IS_PACKAGED_FOR_DISTRIBUTION]
    if is_packaged_for_distribution == 1:
        return True
    return False

Win64\common\tests\test_file_helper.py


def test_get_packaged_for_distribution_bool():
    pre_session_json_file_name = file_helper.const.SESSION_JSON_FILE_NAME
    file_helper.const.SESSION_JSON_FILE_NAME = 'test_game_session.json'
    file_path = file_helper.get_session_json_file_path()
    if os.path.exists(file_path):
        os.remove(file_path)

    #Check the return value if the file does not exist.
    is_packaged_for_distribution = \
        file_helper.get_packaged_for_distribution_bool()
    assert_false(is_packaged_for_distribution)

    #If the file exists but an empty string is set
    #Check the returned value.
    with open(file_path, 'w') as f:
        f.write('')
    is_packaged_for_distribution = \
        file_helper.get_packaged_for_distribution_bool()
    assert_false(is_packaged_for_distribution)

    #Check the return value when the value is set to 1.
    with open(file_path, 'w') as f:
        session_data_dict = {
            const.SESSION_DATA_KEY_IS_PACKAGED_FOR_DISTRIBUTION: 1,
        }
        json.dump(session_data_dict, f)
    is_packaged_for_distribution = \
        file_helper.get_packaged_for_distribution_bool()
    assert_true(
        is_packaged_for_distribution
    )

    #Check the return value when the value is set to 0.
    with open(file_path, 'w') as f:
        session_data_dict = {
            const.SESSION_DATA_KEY_IS_PACKAGED_FOR_DISTRIBUTION: 0,
        }
        json.dump(session_data_dict, f)
    is_packaged_for_distribution = \
        file_helper.get_packaged_for_distribution_bool()
    assert_false(is_packaged_for_distribution)

    os.remove(file_path)
    file_helper.const.SESSION_JSON_FILE_NAME = pre_session_json_file_name

Place branches in various parts of the test runner as shown below.

Win64\common\python_test_runner.py


def run_tests(ue, target_module):
    ...
    is_packaged_for_distribution = \
        file_helper.get_packaged_for_distribution_bool()
    if is_packaged_for_distribution:
        return
    ...

Reflect the random state of the initial display

Currently, it starts with the colors aligned, so let's rotate it randomly like a normal Rubik's cube.

We have an immediate rotation process that doesn't animate, so we'll use that to add functions to the blueprint. Also, since the function name is reset in the Gym library of OpenAI for the environment reset process, set it as reset according to that (it is used not only at the beginning but also when moving it again).

First, get the value of how many times to turn. It seems that something like NumPy's randint is prepared in the Random Integer node, so use that. It seems that the minimum value is set to 0 and the maximum value is set to Max -1. Also, if it is left as it is, when 0 comes out, the surface will be aligned from the beginning ..., so it should be rotated at least 200 times across the MAX node.

image.png

Prepare a boolean value as a local variable to determine in which direction to rotate in the loop. Also, set it to False at the beginning of the loop.

image.png

First of all, XYZ Randomly decide what to make True by the boolean value of which direction to rotate. Try using the Switch node for the first time. If you are accustomed to programming, you can use it without any trouble.

image.png

Then the direction of rotation. Randomly decide whether to rotate left or right, or up or down.

image.png

Finally, calculate the values such as which column to rotate. Since it is necessary to calculate a value from 1 to 3, the value from 0 to 2 is output by the Random Integer node and then incremented by 1.

image.png

After that, branch by Branch according to the determined value and execute the required rotation, and it is completed.

image.png

I will move it.

image.png

The color came off nicely. I'm not sure how to rotate it so that all the colors are aligned.

Try running it again.

image.png

It feels different from the previous one. Sounds okay. Although it is a process with a lot of loops, in my desktop environment it ends in an instant, and it is not necessary to maintain FPS in particular, and in the first place, the person who moves reinforcement learning by learning is the desktop, and a PC with reasonably good specifications Most of them will be, so I judge that there is no problem.

Prepare a process to acquire the boolean value of whether or not it is rotating.

I'm having trouble if another rotation is performed during the animation. Eventually, I will try to control errors on the Python side, but for the time being, I will add an implementation on the blueprint and write Python code in cooperation with that.

The name will be created with a blueprint that inherits PyActor named BP_Action according to the terms of the Gym library.

It's okay to add a blueprint, but by the way, how do you get the actors on the level ...? When I thought, it was written in the official document.

Search for Actors in Blueprints

It seems that you can use the Get All Actors Of Class node, so I will try it.

image.png

It is a node that simply turns in a loop and prints the name on the screen.

image.png

It is displayed properly. Easy.

With this, it seems possible to have a function in the BP_Action blueprint to take the value of whether or not the actor of the rotating cube exists.

Add a function called isRotating to the blueprint of the base class of BP_CubeBase.

Since we had previously prepared a function to get the truth value of the target rotation in each rotation direction, we will prepare an array of truth values with local variables and integrate the array while calling it. It seems that you can add an array to an array with the APPEND node (extend-like behavior in Python).

image.png

When I finished adding a set of boolean values to the array, I used to prepare a function that returns True if one of the boolean values is True in the array by passing it in another place (that). I specified 3 arrays, but ...), so I will use that.

image.png

Create a blueprint for BP_Action with a function named isAnyCubeRotating.

image.png

Get the array using Get All Actors Of Class, which I mentioned a while ago, and loop it around.

image.png

If there is a cube that is rotating in the loop, it will be sent to the True Return Node, and if no rotating one is found and the loop ends, it will be sent to the False Return Node.

Also, in the BP_Action blueprint, specify the Python module and class.

image.png

Add a module called action.py on the Python side and write the process. I haven't written the rotation process etc. from the Python side yet, so I will skip the test etc. once (I will write it if the functional test related is in place, but I understand well that it is not in place and UE4 related things Because not). Also, try the console output with tick (for now it should always return False).

Content\Scripts\action.py


"""A module that describes some behavior-related processing in the Agent.
"""

import unreal_engine as ue


class Action:

    def tick(self, delta_time):
        """
A function that is executed approximately every frame during game play.

        Parameters
        ----------
        delta_time : float
Elapsed seconds since the last tick call.
        """
        ue.log(is_any_cube_rotating(action_instance=self))


def is_any_cube_rotating(action_instance):
    """
Gets the boolean value of whether any cube is spinning.

    Parameters
    ----------
    action_instance : Action
An instance of the Action class with a uobject.

    Returns
    ----------
    is_rotating : bool
Set to True if any cube is spinning.
    """
    is_rotating = action_instance.uobject.isAnyCubeRotating()[0]
    return is_rotating

If you look at the log, you can see that False is output.

...
LogPython: False
LogPython: False
LogPython: False
LogPython: False
...

It seems to be okay for the time being. Turn off the console output and go on to the next.

Put NumPy in

When I uninstalled the hdf5 relationship, I also uninstalled NumPy installed as dependencies, but after all I wanted to use it, so I will put only NumPy.

$ ./python.exe -m pip install --target . numpy
Successfully installed numpy-1.17.3

Advance the implementation of Action-related control.

First of all, I defined the allocation of Action numbers. Make sure to check for duplicates and whether they are properly included in the list.

Content\Scripts\action.py


import numpy as np
...
ACTION_ROTATE_X_LEFT_1 = 1
ACTION_ROTATE_X_LEFT_2 = 2
ACTION_ROTATE_X_LEFT_3 = 3
ACTION_ROTATE_X_RIGHT_1 = 4
ACTION_ROTATE_X_RIGHT_2 = 5
ACTION_ROTATE_X_RIGHT_3 = 6
ACTION_ROTATE_Y_UP_1 = 7
ACTION_ROTATE_Y_UP_2 = 8
ACTION_ROTATE_Y_UP_3 = 9
ACTION_ROTATE_Y_DOWN_1 = 10
ACTION_ROTATE_Y_DOWN_2 = 11
ACTION_ROTATE_Y_DOWN_3 = 12
ACTION_ROTATE_Z_UP_1 = 13
ACTION_ROTATE_Z_UP_2 = 14
ACTION_ROTATE_Z_UP_3 = 15
ACTION_ROTATE_Z_DOWN_1 = 16
ACTION_ROTATE_Z_DOWN_2 = 17
ACTION_ROTATE_Z_DOWN_3 = 18

ACTION_LIST = [
    ACTION_ROTATE_X_LEFT_1,
    ACTION_ROTATE_X_LEFT_2,
    ACTION_ROTATE_X_LEFT_3,
    ACTION_ROTATE_X_RIGHT_1,
    ACTION_ROTATE_X_RIGHT_2,
    ACTION_ROTATE_X_RIGHT_3,
    ACTION_ROTATE_Y_UP_1,
    ACTION_ROTATE_Y_UP_2,
    ACTION_ROTATE_Y_UP_3,
    ACTION_ROTATE_Y_DOWN_1,
    ACTION_ROTATE_Y_DOWN_2,
    ACTION_ROTATE_Y_DOWN_3,
    ACTION_ROTATE_Z_UP_1,
    ACTION_ROTATE_Z_UP_2,
    ACTION_ROTATE_Z_UP_3,
    ACTION_ROTATE_Z_DOWN_1,
    ACTION_ROTATE_Z_DOWN_2,
    ACTION_ROTATE_Z_DOWN_3,
]
...
def test_ACTION_LIST():
    assert_equal(
        len(ACTION_LIST), len(np.unique(ACTION_LIST))
    )
    members = inspect.getmembers(sys.modules[__name__])
    for obj_name, obj_val in members:
        if not obj_name.startswith('ACTION_ROTATE_'):
            continue
        assert_true(isinstance(obj_val, int))
        is_in = obj_val in ACTION_LIST
        assert_true(is_in)


python_test_runner.run_tests(
    ue=ue,
    target_module=sys.modules[__name__])

Add the function of each action to BP_Action according to the defined constant name. I thought, but the function to calculate the list of target cubes by rotation was written in the level blueprint. I have the impression that calling that function from BP_Action seems to be a hassle ... (I'm confused because I can't refer to this until I get used to it ...) Reference: Consider how to refer to the level blueprint

I can't help mourning, so I'll add a function to the blueprint of the base class of the cube first to get the boolean value of whether it is the target cube at each rotation (then the cube in the level Can be taken with the Get All Actors Of Class node ...).

Try the function library

Before proceeding, I wrote assert helper-like things in the level blueprint, but it may be inconvenient because it can not be used in the BP class, so I will consider adjusting it.

When I looked it up, it seems that there is a function library.

What is a function library? A blueprint that allows you to have various functions that can be accessed from anywhere in one place. Unlike regular blueprints, it cannot hold variables and there is no event graph. You can't create macros, only functions. The functions written in this will be usable regardless of the blueprint, actor or level. [UE4] Good use of function library and macro library

How to make it was written in the following article: bow:

Create UE4 function library (Blueprint Function Library)

By convention, what should the function library folder be named ...? In videos and books, it was introduced that blueprints should be BluePrints, file names should be prefixed with BP_, materials should be Materials, etc., but how was the function library in the first place? Was the function library introduced ... (It's not good to forget it if you don't output it ...)

For the time being, it's not a job, so feel free to name it the Library. Try to prefix the file name with LIB_.

image.png

It seems that you can create it by adding a new file in the content browser and selecting Blueprints → Blueprint Function Library.

I named it LIB_Testing because I want to add test relationships.

image.png

When I open it, it looks like this. It looks like a blueprint with a very simple structure consisting of only functions and local variables. Move the function you want to move here, and replace the existing function on the level BP there.

It seems that the functions added to the library can be called from the level BP or other BP classes as they are without doing anything special.

image.png

This makes it easier to check light values in the BP class.

Add the process of getting the boolean value of whether or not it is the target of rotation to the base class of the cube.

As mentioned above, it is possible to get the boolean value of whether or not it is the target of rotation from the base class of BP_CubeBase, and it can be called from BP_Action.

Since we will create a function that returns a boolean value as shown below in the range of 1 to 3 in XYZ, we will add 9 functions.

image.png

The common part is separated into different functions. Also, the array of the position type of the target cube in the rotation of the target is a constant and has already been prepared before, so we will use that.

image.png

Inside the common process, turn the loop for the array and

image.png

If the type value of the position of the current cube matches the value of the array of indexes in the current loop, True is returned, and if there is no corresponding value even after the loop ends, False is returned.

I will omit the details, but I will write a test to confirm the behavior of the process to some extent.

image.png

Now that we have 9 rotations ready and the test doesn't get caught, we can move on to the next. Let's try adding one rotation function to BP_Action. First of all, we will proceed from the simple non-animated type of rotation.

First, get the actor of the cube and turn the loop.

image.png

After that, set a branch with the truth value of whether it is the rotation target prepared earlier, and if it is True, rotate it.

image.png

I will try it on the Python side as well.

Content\Scripts\action.py


class Action:

    total_delta_time = 0
    is_rotated = False

    def tick(self, delta_time):
        """
A function that is executed approximately every frame during game play.

        Parameters
        ----------
        delta_time : float
Elapsed seconds since the last tick call.
        """
        self.total_delta_time += delta_time
        if self.total_delta_time > 5 and not self.is_rotated:
            self.uobject.rotateXLeftImmediately1()
            self.is_rotated = True

With this, for the time being, it will rotate instantly once every 5 seconds.

20191110_2.gif

I tried to preview it, but it looks okay. We will build other rotation processing in the same way, but before that, move the function for testing the rotation result to the function library side so that it can be referred from the BP_Action side, and replace the function on the level side. I will cut it off.

After moving, add a function for checking the value after rotation of the target to the end of the function added to BP_Action this time, and preview it to make sure that it does not get caught in the check.

image.png

It seems to be okay for the time being, so add a series of rotations in other directions and call it from the Python side to check the operation.

20191112_1.gif

The process of immediate rotation via Python seems okay. Next time, I'll work on connecting the animated rotation process to Python (the article is getting longer, so I'll leave this article around here).

Points of concern

--Occasionally, Python script updates were not reflected (restarting UE4 will fix it). Since the Python process remains running, I was troubled by cases such as various inconsistent and unsuccessful processing if left as it is for a long time. There are cases where it cannot be fixed even if you use importlib. Isn't it that it takes a long time to start up? I'm a little worried. I wonder if there is a good way to do it ... (I think it's okay to catch an issue on github, a plugin for Unreal Engine Python later ...)

Reference page summary

Recommended Posts

Beginners want to make something like a Rubik's cube with UE4 and make it a library for reinforcement learning # 4
Beginners want to make something like a Rubik's cube with UE4 and make it a library for reinforcement learning # 5
Beginners want to make something like a Rubik's cube with UE4 and make it a library for reinforcement learning # 6
I want to climb a mountain with reinforcement learning
[Introduction] I want to make a Mastodon Bot with Python! 【Beginners】
Reinforcement learning 35 python Local development, paste a link to myModule and import it.
I want to make a game with Python
I want to write an element to a file with numpy and check it.
Machine learning beginners tried to make a horse racing prediction model with python
Load a photo and make a handwritten sketch. With zoom function. Tried to make it.
I tried to make something like a chatbot with the Seq2Seq model of TensorFlow
I tried to create a reinforcement learning environment for Othello with Open AI gym
Machine learning beginners try to make a decision tree
How to interactively draw a machine learning pipeline with scikit-learn and save it in HTML
I want to make a voice changer using Python and SPTK with reference to a famous site
Associate Python Enum with a function and make it Callable
Experiment to make a self-catering PDF for Kindle with Python
I want to make a click macro with pyautogui (desire)
For those who want to start machine learning with TensorFlow2
I want to make a click macro with pyautogui (outlook)
Library for specifying a name server and dig with python
PyPI registration steps for those who want to make a PyPI debut
Make it possible to output a log to a file with go echo
How to make a surveillance camera (Security Camera) with Opencv and Python
Make a thermometer with Raspberry Pi and make it viewable with a browser Part 4
[For beginners] How to register a library created in Python in PyPI
I tried to make a periodical process with Selenium and Python
I want to create a pipfile and reflect it in docker
Throw something to Kinesis with python and make sure it's in
Try to make a blackjack strategy by reinforcement learning ((1) Implementation of blackjack)
I tried to make a strange quote for Jojo with LSTM
[Concept] A strategy to analyze data with python and aim for a decline after shareholder benefits to make a profit
Is it possible to enter a venture before listing and make a lot of money with stock options?
[Hi Py (Part 1)] I want to make something for the time being, so first set a goal.
TF2RL: Reinforcement learning library for TensorFlow2.x
<For beginners> python library <For machine learning>
2. Make a decision tree from 0 with Python and understand it (2. Python program basics)
Python beginners decided to make a LINE bot with Flask (Flask rough commentary)
A collection of tips for speeding up learning and reasoning with PyTorch
I tried to make a calculator with Tkinter so I will write it
How to make a container name a subdomain and make it accessible in Docker
Memorandum of means when you want to make machine learning with 50 images
Make a decision tree from 0 with Python and understand it (4. Data structure)
I want to make a web application using React and Python flask
I thought I could make a nice gitignore editor, so I tried to make something like MVP for the time being