[PyModule] Record inputs while playing game and re-play the input sequence !

Hello, I just wrote a little module which will be useful. I hope.
I want to post it on this forum to thank blenderartists.org for all the help it gave to me !

So, to be exact, this module not intend to record inputs, but it records logic controllers outputs.
And it can play the output sequence again.

I wrote it to record my actions when I move my animated character around the scene, and to re play the “animation” in order to have an animated Non Player Character.

It may be used for other applications, it’s up to you !

So, this is the module file:


# Action Record and Play Module
# Provides two classes to record
# and re-play the output of
# controllers bricks.
# Useful to record an in-game behavior,
# save it into file and re-play it later.

# This module can be accessed by a python controller with
# its execution method set to 'Module'
# * Set the module string to "gamelogic_module.main" (without quotes)
# * When renaming the script it MUST have a .py extension
# * External text modules are supported as long as they are at
#   the same location as the blendfile or one of its libraries.

import bge
import pickle
import os.path

# Class to record the controllers's outputs.
# Connect all needed sensors to the python brick.
# Write a function to simulate controllers.
# The record is saved when game exits.
class ActionRecorder:
    
    # Constructor
    # filepath [string] Filepath to save record. Can be None.
    # to_stdout [bool] Write the python array to stdout during the recording.
    # prop_timer [string] Name of the object's property which is a timer.
    # nb_sensors [uint] Number of sensors connected to the python brick.
    # nb_controllers [uint] Number of controller to record.
    # fct_output [fct] Function to call to compute a controller output.
    #    It must return a boolean.
    #    Its prototype must be: my_func(index, fct_input) with
    #    index [uint] Index of the controller.
    #    fct_input [fct] Callable function  to get a sensor state.
    #        See <ActionRecorder._input>
    def __init__(self, filepath, to_stdout, prop_timer, nb_sensors, nb_controllers, fct_output):
        self._filepath = filepath
        self._to_stdout = to_stdout
        self._prop_timer = prop_timer
        self._nb_sensors = nb_sensors
        self._nb_controllers = nb_controllers
        self._fct_output = fct_output
        self._sensors = None
        self._outputs = [] # Boolean state of previous outputs
        for i in range(self._nb_controllers):
            self._outputs.append(False)
        self._keyframes = []

    # Private function
    # Get a sensor state.
    # index [uint/str] Index of the sensor.
    # [return] [bool] Value of the signal sensor.
    def _input(self, index):
        if (index >= self._nb_sensors) or (self._sensors is None):
            return False
        else:
            return self._sensors[index].positive

    # Record outputs. Call this method regularly.
    # cont [Obj] Python controller.
    def rec(self, cont):
        self._sensors = cont.sensors
        
        for index in range(self._nb_controllers):    
            out = self._fct_output(index, self._input)
            
            if(out != self._outputs[index]): # Signal state changed
                self._outputs[index] = out
                time = cont.owner[self._prop_timer]
                self._keyframes.append([time, index, out])
                
                if (self._to_stdout):
                    print('['+str(time)+','+str(index)+','+str(out)+'],', end="",flush=True)
    
    # Destructor.
    # Save record to file if it was specified.
    def __del__(self):
        if ((self._filepath is not None) and (len(self._keyframes) > 0)):
            file = open(self._filepath, 'wb')
            pickle.dump(self._keyframes, file, 2)
            file.close()


# Class to play a record made with ActionRecorder.
# Simulate controllers' outputs.
# Connect an "Always" sensor with True pulse mode to the python brick.
# Connect all your actuators to the py brick.
class ActionPlayer:
    
    # Constructor.
    # filepath [string] File which contains keyframes to load. 
    #    Can be None, see <ActionPlayer.set_keyframes>
    # pro
    # prop_timer [string] Name of the object's property which is a timer.
    #    The initial value can be an offset for the playing.
    # nb_controllers [uint] Number of controller to replay.
    # nb_actuators [uint] Number of actuators connected to the python brick.
    # fct_ln [fct] Function to call when a virtual output changed. This function
    #     will transfer the signal to the right actuators.
    #    Its prototype must be: my_func(index, fct_send, signal) with
    #        index [uint] Index of the virtual controller.
    #        fct_send [fct] Callable function to send the signal to an actuator.
    #            See <ActionPlayer._send>
    #        signal [bool] Value of the virtual output after change.
    def __init__(self, filepath, prop_timer, nb_controllers, nb_actuators, fct_ln):
        self._cont = None
        self._actuators = None
        self._prop_timer = prop_timer
        self._nb_controllers = nb_controllers
        self._nb_actuators = nb_actuators
        self._fct_ln = fct_ln
        self._keyframes = []
        if (filepath is not None):
            self._load_keyframes(filepath)
        self._current = 0 # current keyframe pointed (to treat)
        self._outputs = [] # Boolean state of simulated controllers outputs
        for i in range(self._nb_controllers):
            self._outputs.append(False)
    
    # Set the keyframes data directly from a python object.
    # The format is the same as produce by the ActionRecorder
    # when output to stdout is enabled. 
    # It is an array of element of this format:
    # [time, controller_index, signal]
    # e.g. [1.0, 2, True], [2.63, 0, False]
    def set_keyframes(self, keyframes):
        if (keyframes is not None):
            self._keyframes = keyframes

    # Private function
    # Load keyframes from a file.
    def _load_keyframes(self, filepath):
        if (os.path.isfile(filepath)):
            file = open(filepath, 'rb')
            self._keyframes = pickle.load(file)
            file.close()

    # Private function
    # Send a virtual controller output to an actuator.
    # index [uint/str] Index of the actuator to (de)activate
    # signal [bool] Signal state to transfer.
    def _send(self, index, signal):
        if (index < self._nb_actuators):
            if (signal):
                self._cont.activate(self._actuators[index])
            else:
                self._cont.deactivate(self._actuators[index])
            
    # Play the record. Call this function regularly
    # to update actuators' activation states.
    # cont [Obj] Python controller
    def play(self, cont):
        if (self._current >= len(self._keyframes)):
            return
        
        self._cont = cont
        self._actuators = cont.actuators
        
        # update outputs
        
        # enum
        # TIME = 0
        # CONTROLLER = 1
        # SIGNAL = 2
        time = cont.owner[self._prop_timer]
        current_keyframe = self._keyframes[self._current]
        
        if (time > current_keyframe[0]): # treat keyframe
            if (current_keyframe[1] < self._nb_controllers):
                self._outputs[current_keyframe[1]] = current_keyframe[2]
            self._current += 1
        
        # update actuators activation state
        for index in range(self._nb_controllers):
            self._fct_ln(index, self._send, self._outputs[index])


And there is a little module example too:


from ActionRecPlay import *

# * rec *

# Redefine this function to simulate controllers outputs.
def rec_player_out(index, fct_input):
    if index == 0:
        return fct_input(0) and not(fct_input(1))
    else:
        return fct_input(index)

rec_player_i = ActionRecorder('player.ar', False, 'timer', 5, 5, rec_player_out)

def rec_player(cont):
    rec_player_i.rec(cont)


# * play *

# Redefine this function to connect virtual outputs
# to actuators
def play_player_ln(index, fct_send, signal):
    fct_send(index, signal)
    if (index < 3):
        fct_send(index+5, signal)
    

play_player_i = ActionPlayer('player.ar', 'timer', 5, 8, play_player_ln)
#play_player_kf = [ [1.0166664123535156,0,True],
#                    [1.5833328247070312,0,False],
#                    [1.8333324909210205,0,True],
#                    [2.6999990940093994,0,False] ]
#play_player_i.set_keyframes(play_player_kf)

def play_player(cont):
    play_player_i.play(cont)



I would like to upload a blend file to show how all of that stuff are connected but it seems I’m a too fresh user !

If you have any suggestion, question or remark (even about my english), I will be happy to read you !

Good day :slight_smile: