englercj / phaser-tiled

A tilemap implementation for phaser focusing on large complex maps built with the Tiled Editor.
MIT License
290 stars 31 forks source link

Tile-Sprite objects are too "fat" #12

Closed pnstickne closed 8 years ago

pnstickne commented 9 years ago

One issue with distinct Tile-as-Sprite objects is that even a little change really adds up. Not an issue on a desktop, but it matters in a more memory-constrained environment.

For instance, a Phaser.Sprite weighs in at a shallow size of 500+ bytes in Chrome (and enough tile-specific information stored without specific consideration adds about 200 additional bytes) - and Chrome is pretty efficient in how it handles objects and "private classes". Thus the memory foot-print for a 256x256 map (having each Tile as a Sprite) is a very conservative ~50MB+ of memory. A 400x400 map would be in excess of 110MB with each Tile as a Sprite.

To combat this a Tile should fundamentally be a "thin" object - perhaps one that can be/is mostly encoded in a [typed/dense] array and/or a few ancillary object look-ups as required. The amount of actual data in a Tile object is only a few tens of bytes (easily less than 1/10th of the data required by a "fat" Sprite, although even less is required).

These "thin" tile objects would only be fetched (to fatter display Sprites) when displayed. The downside is the loss of truly ad-hoc objects (which arguable is not well-suited to a Tile anyway).

englercj commented 9 years ago

Good callout, I wrote the sprites in the way they are because this is how they are in Phaser (minus the extending of a sprite base class). It does make sense to try and make them more thin though as a memory optimization.

Thanks for the suggestion I will look at getting this in for 2.0.

pnstickne commented 9 years ago

I like the idea of using the separate sprites and direct-rendering thereof;

It just seems like it might be coupling too much with the display/graph to establish a Tile-is-a-Sprite and a Tilemap-is-a-Group.

Keeping the current Tile/Tilemap structure as it is currently in Phaser would keep existing code working without any (or with minimal) modifications; the improved changes would be for a TileSprite (eg. Sprite with Tile property) and a Tilelayer/TileSpriteRenderer/TilemapDisplay^1

The actual render/layout component would then only be responsible for (intelligently) creating the corresponding TileSprites from the given Tiles/region. This could be done on-demand and/or utilize a cache thereby reducing the number of required display objects, start-up overhead, and per-tile memory utilization.^2

The only issue I know of with such an approach is that marking a specific tile or region is not exposed with the Tilemap (so it would require a forced-refresh of all displayed Sprites), but that is only related to the displayed subset and can be resolved with a more intelligent Tilemap that reports dirty regions/tiles (which should eventually be done anyway).

^1 I can't think of a good name, but It's not really a layer if there is a Sprite/Tile separation; it's the rendered/displayed result of a layer, or layers with future improvements; the current TilemapLayer class also feels misnamed.

^2 There appears to be requests for "many" tile layers - memory overhead and ways to minimize/share data becomes a larger factor when striving to also partially address those.

englercj commented 9 years ago

So that is how the implementation was originally, only the number of tiles displayed on the screen are actually created; then their textures are just swapped around as panning happens.

The reason I changed it to create all the tiles at startup and show/hide them is because this is consistent with the current Phaser implementation. There are tilemap functions that access/modify tiles that need to exist (or be lazy created).

The naming of objects in this library is based on the object from Tiled Editor that they represent. A Tilelayer is a Tilelayer from Tiled, and so on.

pnstickne commented 9 years ago

My proposition is that the existing implementation (logic) is only for the Tile data itself, not for the Sprites. This already works and can be optimized (or extended) independently of the final display mechanism. The point is to keep a separation of the the Tile data and the display of such - I'm entirely in support of this (original) approach and working with the [Phaser-native] Tile/Tilemap objects!

The Sprite layer/render would be responsible for transforming any relevant updates to the view port (and cache such however is deemed fit) but otherwise does not support and Tile/Layer logic or direct manipulation of such - all changes and interactions must be done through the Tilemap/Tile/logic layer itself.

The only time that all the displayed tiles is when the layer is marked as dirty, this can be fixed/reduced with better Tilemap support for dirty regions/tiles. Otherwise it is sufficient to pull in the appropriate, and possibly changed, Tile information for delta-edge updates. Since the big slowdown with tilemap rendering is during scrolling this should not affect the improved performance overall.

englercj commented 9 years ago

Sounds interesting, how do you feel about putting together a PR?

pnstickne commented 9 years ago

I've "merged" it into https://github.com/pnstickne/phaser/tree/wip-tiles - it's not a PR because it uses a heavily modified (and [mostly] API-compatible) base which diverges from Phaser dev.

This modified base works to refine the API contracts - it provides some guarantees and restricts some usage (which ought to fall mostly inline with current "working practices"), creates a thin Tile class (with usage restrictions) with potential for typearray/array-backing, and creates a refined TileLayer-logic API (most Tilemap work is proxied now). There are also several bug-fixes and some minor performance improvements; the only known regression is tile callbacks. The only known API difference is the redefinition of Tile#properties.

Then I've added (butchered, mind you!) the Tilelayer from this project as TilemapSpriteRenderer and stripped down Tile (called TileSprite) to a bare minimum; I pulled in the delta-edge canvas updates from another WIP as TilemapCanvasRenderer. (I still don't like the names, but the idea is that these are just DisplayObjects to add to the scene that don't expose any additional layer operations; besides what the old TilemapLayer exposed.)

I did not integrate any of the new Tiled features / parsing from this project as I am awaiting your feedback. (Actually, I flat out ripped them out to get the proof-of-concept running.)

Do note that I probably butchered the rendering/display the implementation - I'm hitting about 20% CPU usage when scrolling, which probably means I ruined an appropriate optimization or am running in the wrong mode or something. (Again, I butchered it enough to "make it work"; but I haven't played around with texture/sprite optimization. For example, I increased the bounds in setupRenderArea - or it wasn't drawing scrolled tiles - and commented out some things like sprite batching..)

I'd really like to get the new Tiled features and different/improved "renderers" into Phaser; one big issue is definitely maintaining decent API compatibility while providing significant improvements and new features - I think this is a chance to do both.

englercj commented 9 years ago

I'm now working on a "thin-tile" implementation that basically is attempting to split the data representation of a tile from the visual sprite used to display it.

The idea is to generate sprites for the viewport, only enough to fille the viewport and a buffer. Then, lazy create Tile objects as they are needed, and no longer have them inherit from Phaser.Sprite. When data changes in a Tile object that change is reflected back down into the underlying data model (tileset and tilelayer) and the visual sprites are updated with the new data. Hopefully this improves the memory footprint and also drastically reduces the startup time for a large tilemap, since the creation of all tiles into full Tile objects is no longer front loaded.

englercj commented 9 years ago

The thin-tiles branch now only creates Phaser.Sprites for the viewport. I'll be adding getters to get a Tile object that exposes the data of a tile in a nice way in case users want to get that data, but it will not be a sprite just a data container.

These changes in addition to some destroy fixes and tileset changes have abolished memory leaks and reduced the memory footprint dramatically. Now only what is needed for rendering is created, and it is created only when it is needed so startup time is faster too!

englercj commented 9 years ago

All I need to do is fix up the physics methods that relied on tile objects existing and then it should be good to go!

englercj commented 8 years ago

Implemented a much lighter approach to tile management in v2