Python plays .reanim animation (3: performance optimization and other details)

Article directory

  • Fix performance issues
  • resource manager

See the code here. Use python -m anp to open a .reanim animation player. Almost all animations can be played after all assets are loaded (which takes a little longer). Play simple animations without loading resources.

Fix performance issues

In the previous two articles, we have completed a player that can play animations with an attacher layer. The fly in the ointment is that the performance is not enough and the frame drop is severe. Here is a way to double the speed.

mypyc compiles Python files into C, and then compiles C into a .pyd dynamic link library, which provides great performance improvement and can be installed using pip install mypy. In the code mentioned at the beginning of this article, mypyc anp/player.py can be compiled without changing anything. According to my observation, it can probably double the performance.

What I found to be careful in actual use (mostly because mypyc uses mypy this strong type checker):

  1. Can’t use metaclasses, so Qt classes like QGraphicsItem that inherit from QGraphicsItem won’t compile (they all have a connector metaclass)
  2. Use assert ... is not None for variables that cannot be None under special conditions, but cannot be automatically inferred from the context to be None code>. Adding if ... is None: return before this statement has the same effect.
  3. For functions decorated with @typing.overload, remember to add / at the end of the parameter list, because mypy checks such a function and will check whether all conform to The declared parameter list (including keyword arguments) conforms to the implementation. For example, def f(a: int, b: int) -> int and def f(c: int, b: int) -> int are inconsistent, because (a=1, b=2) satisfies the first definition, but not the second. And def f(a: int, b: int, /) -> int and def f(c: int, b: int, /) are consistent.

Explorer

The resource manager Resources was used in the previous article, and the implementation method is provided here.

Resources uses the singleton pattern, use Resources.instances() to get the instance. Inside there are anim_cache, img_cache to cache animations and pictures, all of which are keyed by name.

_resources: 'Resources | None' = None
resources_root: Path = Path() # Fill in the folder where the executable program of Plants vs. Zombies is located
REANIM_HEAD: Final = 'IMAGE_REANIM_'
class Resources:
    def __init__(self):
        self.img_chache: dict[str, QPixmap] = {<!-- -->}
        self.anim_cache: dict[str, Animation] = {<!-- -->}

@staticmethod
    def instance():
        if _resources is not None:
            return _resources # Lazy loading, you can also use _resources = Resources() at the top level of the module to initialize when importing
        _resources = Resources()
        return_resources

    def load_img(self, name: str):
        if name in self.img_cache:
            return self.img_cache[name]
        if path := self.find(name): # find: find the corresponding resource path according to a name, return None if not found
            img = QPixmap(path)
        elif name.startswith(REANIM_HEAD): # Resources whose names start with IMAGE_REANIM_ will not be found and need to be filled manually
            img = QPixmap(resources_root / 'reanim' / name[len(REANIM_HEAD):])
        else:
            img = None
        if img is None or img.isNull():
            # draw a picture if not found
            img = QPixmap(100, 100)
            img.fill(Qt.white)
            painter = QPainter(img)
            painter. drawText(10, 50, name)
            del painter # QPainter is always on and prone to memory problems
        self.img_cache[name] = img # cache image
        return img
    
    def load_reanim(self, name: str):
        if name in self.anim_cache:
            return self.anim_cache[name]
        anim = Reanim.load(resources_root / 'reanim' / f'{<!-- -->name}.reanim')
        self,anim_cache[name] = anim
        return anim

Inside the folder where the Plants vs. Zombies executable is located, there is a properties subfolder. The most important ones are LawnStrings.txt (save the strings used in the game, such as illustrations, wisdom tree, etc.) and resources.xml. resources.xml contains the mapping from the name of the picture used in the game to the path, and also contains sound effects, fonts and additional data, such as the number of columns and rows of the picture. Here is the simplest version of the resource loader.

The top level of resources is the tag pair. It contains various blocks, surrounded by tags, such as Init for the picture (PopCap Logo) that appears when opening the game, LoaderBar Images used for loading (such as a sod animation and title image).

Each resource block contains several resources. There are different types of resources, which are represented by different tag pairs. For example, tag pairs represent pictures, and tag pairs represent sound effects. But each resource tag pair has no end tag, that is, in this form. Each resource contains id and path attributes, which represent the name and path of the resource (the path may not have a suffix). For convenience, in addition to resources, the resource block can also contain to set default values. This also has no closing tag. It can contain the attributes id_prefix and path. When a resource is present, its name is rewritten to the current id_prefix with its own id attribute, and similarly for path.

The tag can also contain attributes such as a8r8g8b8, but I don’t know what most of them do yet.

from xml.etree.ElementTree import fromstring, Element

class Resources:
    ... # Same as above

    def load_properties(self, path: str | Path): return self.load_properties_from_str((Path(path) if isinstance(path, str) else path).read_text('utf-8'))

    def load_properties_from_str(self, s: str):
        calc = _PropertiesCalcuator(fromstring(s))
        self.all = calc.start() # Add the all attribute, including the resource name-to-path mapping of each sub-block
    
    def find(self, name: str):
        for block in self.all.values():
            if name in block:
                return block[name]

class _PropertiesCalcuator:
    def __init__(self, ele: Element):
        self.top = ele
        self. reset()
    
    def reset(self):
        self.id_prefix = ''
        self. path = Path()
    
    def start(self):
        return self. resource_manifest(self. top)
    
    def resource_manifest(self, tree: Element):
        assert tree.tag == 'ResourceManifest'
        res: dict[str, dict[str, Path]] = {<!-- -->}
        for resource in tree:
            self. reset()
            name = resource. get('id')
            assert name is not None
            res[name] = self. resource(resource)
        return res

    ... # See the code on Github