July 27, 2007 will

Transparently Recording Game Demos

I have many ideas in the course of any given day. They are dredged up from the depths of my subconscious and placed on my mental in-tray, to be sorted in to mental heaps. Most are placed on to the "that'll never work" heap, others are thrown on the "better not, that's probably illegal" heap or the "plutonium isn't available at every corner drug store you know" heap. Occasionally though, some make it to my mental out-tray.

One recent idea was about recording demos in a game written with PyGame, which is something that is not that hard to do, but requires a bit of forethought to reliably record and play back sequences of events. It would be nice if there were some way of transparently recording the on-screen action and playing it back like a movie. Recording a video file on the fly is possible, but it would probably be too processor intensive. What would be better, is the ability to store and time-stamp the function calls used to create the display/audio so that they could later be streamed back in from a file. There are actualy some commercial tools that do this with OpenGL, by providing a proxy DLL that hooks in to the GL function calls - but I figured it could be done in straight Python, given its highly dynamic nature.

A little coding convinced me that this is the case. I wrote a script that can be used to replace every callable in a module or class with a proxy callable that can record the name of the function that has been called and the parameters that it was called with. I tried it with one of my OpenGL demonstrations and it worked perfectly, the demo ran as normal, but it streamed details about every GL function call. All it took in the OpenGL code was replacing two imports with a proxy_import function I had written (see below).

#from OpenGL.GL import *
#from OpenGL.GLU import *

import pystream
pystream.proxy_import('OpenGL.GL', '*')
pystream.proxy_import('OpenGL.GLU', '*')

I'm reasonably satisfied that recording the OpenGL output of a game is practical, but I'm in no hurry to implement it in full since I have plenty of work to do on other projects. The code I wrote to hook in to a module strikes me as something that could have a number of potential uses - beyond OpenGL and games. For instance, it could be used to turn a class in to a remotely callable object. The code is below.

class FunctionProxy(object):

    def __init__(self, name, function):

        self.__name = name
        self.__function = function

    def __call__(self, *args, **kwargs):

        print self.__name
        return self.__function(*args, **kwargs)


class ObjectProxy(object):

    def __init__(self, my_object, name=None, scope=None):

        self.__object = my_object
        self.__dir = self.__object.__dict__.keys()
        self.__name = name

        for symbol in self.__dir:

            attr = getattr(self.__object, symbol)

            func_name = symbol
            if self.__name is not None:
                func_name = self.__name + '.' + func_name

            if hasattr(attr, '__call__'):

                function_proxy = FunctionProxy(func_name, attr)
                setattr(self, symbol, function_proxy)

                if scope is not None:
                    scope[func_name] = function_proxy

            else:

                setattr(self, symbol, attr)
                if scope is not None and not func_name.startswith('__'):
                    scope[func_name] = attr

def proxy_import(module_name, from_list=None, scope=None):

    if from_list is not None:
        from_list = [f.strip() for f in from_list.split(',')]

    if scope is None:
        import sys
        scope = sys._getframe().f_back.f_globals

    module = __import__(module_name, scope, None, from_list)

    if from_list is None:
        proxy = ObjectProxy(module, name=module_name)
        scope[module_name] = proxy
    elif from_list and from_list[0]=='*':
        proxy = ObjectProxy(module, scope=scope)
    else:
        proxy = ObjectProxy(module)
        for symbol in from_list:
            scope[symbol] = getattr(proxy, symbol, scope=scope)
    return proxy

if __name__ == "__main__":

    #from math import *
    proxy_import('math', '*')
    print abs(sin(radians(60)))
Download this code Update: Fixed some nonsense code in the FunctionProxy class (never code after midnight).
Use Markdown for formatting
*Italic* **Bold** `inline code` Links to [Google](http://www.google.com) > This is a quote > ```python import this ```
your comment will be previewed here
gravatar
Gary Bernhardt

This is a great idea! I've often had problems with playing back demos across versions of a game. They don't work across major versions for the whole Quake series of games, for example, and some Half-Life mods don't even play demos back properly in the same version.

To make this a full, freestanding game demo recorder, it would probably be pretty easy to analyze the sequence of function calls and build a list of media assets. The resulting demo could be totally standalone. Being able to replay CAL matches in a browser plugin would be amazing.

Unfortunately, one thing you don't get is the ability to move around and view the demo from any angle like HLTV allows.

gravatar

[...] point out this post not to comment on its subject but just as an example of Python code and to remark that the [...]