Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add layer offsets, dynamic object/tile creation, isometric support, and TexturePacker support #1

Merged
merged 141 commits into from
Feb 21, 2019

Conversation

timothymtorres
Copy link
Collaborator

Consider this PR an unfinished WIP, since I"m trying to get feedback for the final changes that need to be made. With that said, I'm going to run through a list of the commits made and explain my reasoning.

Layer Offsets

Starting with the first few commits, I added layer offsets to all objects. It was trivial to add and implement this feature.

In Tiled a user can manipulate a layer by horizontal or vertical offsets. Neither Berry or Pontiled surprisingly supported this feature. While I doubt many users will absolutely need this feature, it is definitely nice to have. (especially useful for isometric layers)

The only things I have tested layer offsets with are isometric and orthogonal tiles. At some point polygons and other objects (text/rectangles) will need to be tested as well to confirm it's working for all objects.

Isometric Support

Commit b17c034 adds support for both regular isometric and isometric staggered map types. This took a while to get working. Isometric staggered was the most challenging because I was not aware there were extra variables involved. Notable Tiled has a staggeraxis and staggerindex variable that appears in the JSON file for only staggered isometric maps. I used Simple Tiled Implementation for Love2D as a reference when making the staggered map type. The relevant code implementation can be found at this location.

I have tested both map types and they appear on screen but the map position is not centered. Definitely going to need to troubleshoot and fix this at some point.

Method map:getObjects Improvements

Commit 169d9e4 lets a user filter an object by either objType or name. Previously you could only filter by both.

Commits 678bf93 and 1877e0e combine getObject functionality into the getObjects method. Both methods were nearly identical and IMO there was no need to distinguish between plurality of the object(s). I wrote a commit message in 1877e0e explaining the finer details how this works and why.

Major Revision Warning

So all my commits after 1877e0e required me to make a major refactor to Berry. I don't expect you to accept all the features past this point. You need to weigh the benefits and whether or not that is the direction you want to take your library. The above commits I'd argue are a standard necessity that add basic features. Everything in my next commits add some features users may want, but stuff they definitely might not need. You may also not like the way I have refactored things, so feel free to say no or request a revision.

With that said, I do like that you have refactored Berry to be a more elegant and smaller version of ponytiled. Ponytiled had a lot of public methods that an end user was not realistically going to be using.

Since my game needs to have dynamic objects generated after a map loads I had to rip out the tile and object creation code and put them into their own functions. This was a complex endeavor because a lot of that code was coupled together.

Another problem I came across was that it was hard to decipher the proper scope for variables being used since they all were declared on the same lines.

    -- Store the flipped states
    local flip, tilesets = {}, data.tilesets
    local tileLayer, objectLayer, layer, tileset, nextTileset, objects, object, image

    if loop1 then
    	if loop2 then
    		if loop3 then
    			for i=1, #loop4 do
    			   -- blah blah blah

When in reality the scopes were something like this:

    -- Store the flipped states
    local tilesets = data.tilesets
    local nextTileset

    if loop1 then
    	if loop2 then
    		local tileLayer, objectLayer
    		if loop3 then
    			local layer
    			local flip = {}
    			local objects
    			for i=1, #loop4 do
    				local object, image
    				-- blah blah blah

Onto the next changes!

Refactor createTile and createObject into their own functions

99b4974 rips out the code in M.new() and puts them into their own functions. The reason we are doing this is so we can reuse the code when we want to dynamically generate objects later after the map has been loaded.

Add map:addLayer method to Berry

Commit b21679d adds a method for a user to add a new object layer after the map has been loaded. Note - the added layer is only an object layer. The only argument is a name, which names the object layer. My reasoning behind this is the only dynamical content a user might want to add is going to be objects. Adding a tile to a tile layer or adding a new tile layer after the map has been loaded seem like it would be pointless not to mention painfully difficult.

Initially I had designed the method to handle both tile and object layers and even a option table for an argument to fill the values, but realistically it would be overkill. Most users are only going to want to add an object at x/y coordinates.

a57aa34 makes the method also returns the layer after it's added. This saves a user from having to write another line of code to get the layer or they can just ignore the returned value. Lua default library does this with some of the functions and I think it's smart.

Change module and map names

e5b85a8 changes the module's name to Map instead of M. I also was in the process of trying to setup the metatable for the map instance, but found out later that a display group already has a metatable! I got around this by adding the Map methods directly to the display group via the function setupDisplayGroup in 14c4b8a

Change variable names to be assigned by pairs and ipairs

I noticed you were ignoring the variable assignment of the pairs and ipairs operators which could have saved a few lines of code. This is one example but there were a few others I encountered.

			for i=1, #info.data do
				-- GID stands for global tile ID
				gid = info.data[i]

Can be rewritten as:

			for i, gid in ipairs(info.data) do -- GID stands for global tile ID

I fixed this with 10638f1 and a few other misc commits.

Attach data variables to map and layer

Since I had split the createTile and createObject into their own functions the data variables were not in scope. To remedy this I attached the variables to where they were needed.

    map.tilesetsDirectory = (tilesetsDirectory and tilesetsDirectory .. '/') or ''
    map.tilesets = data.tilesets
    map.orientation = data.orientation			
    map.staggeraxis = data.staggeraxis
    map.staggerindex = data.staggerindex
    map.tilewidth = data.tilewidth
    map.tileheight = data.tileheight
		-- Apply properties from info
	    layer.name       = info.name
	    layer.type       = info.type
	    layer.alpha      = info.opacity
	    layer.isVisible  = info.visible
            layer.offset_x   = info.offsetx or 0
	    layer.offset_y   = info.offsety or 0
	    layer.properties = info.properties or {} -- Make sure we have a properties table

Change tileLayer, objectLayer, and layer

There was no need to have a different display group for tileLayer and objectLayer. Additionally the layer variable name was taken by the json.layer.data, so I just renamed this to info. Now layer is the sole display group for both tileLayer and objectLayer. This made the code shorter and easier to understand IMO. I kept getting confused which layer was doing what. These changes happened in 99164c0 but I forgot to mention it.

The previous condition for the if loop ONLY worked if both the objType
and name were BOTH present.  If a user wanted to filter by just the
objType or just the name, it was not possible.  I've rewritten the code
to allow this feature.
This method is nearly identical to map:getObjects and should be removed.
So by removing getObject and unpacking getObjects you can obtain the
same functionality very easily.

If a user just wants a single object all they need to do is:

single_object = map:getObjects()

Regardless if other objects match, they will be discarded as the
variable assignement will only happen to the first object in the table.
The same is true for the second, third, etc. objects.

Ie.  object_a, object_b, object_c = map:getObjects()

If a user desires multiple objects then they can pack them into a table
easily:

object_list = { map:getObjects() }
Refactored the code so that the object creation was put into a local
function.  This function is then copied to the map method.
This method assumes the player is only going to be creating an object
layer using a name as an argument.
This makes method declaration easier.  Additionally this decouples the
initialization process from everything else.  This was becoming a
problem when I was trying to reuse code in the initialization for
methods, so this way will allow me to get around that without changing
too much.  (other than making a neater code structure)
Also changed map.group to be the display.newGroup().  This may change
later if I can find a solution so that the display.newGroup() metatable
doesn't conflict with the Map metamethods and metatable.
Already been declared via ipairs loop below
Trying to cleanup the Map:new function
Saves us some code since both layer.types get put into map group
regardless.
Also applied saved json data to map table.
@timothymtorres
Copy link
Collaborator Author

timothymtorres commented Jan 16, 2019

TO DO LIST

  • Get Feedback on PR
  • Fix style/format code issues
  • Add layer offsets to layer.x and layer.y
  • Rewrite isometric math to be simpler
  • Add Texture Packer Support
  • Fix isometric map positioning
  • Test layer offsets with polygon, text, and other object types
  • Consider making createTile and createObject into local functions

ldurniat/berry.lua Outdated Show resolved Hide resolved
ldurniat/berry.lua Outdated Show resolved Hide resolved
ldurniat/berry.lua Outdated Show resolved Hide resolved
ldurniat/berry.lua Outdated Show resolved Hide resolved
ldurniat/berry.lua Outdated Show resolved Hide resolved
@timothymtorres
Copy link
Collaborator Author

timothymtorres commented Feb 13, 2019

Alright so here is a rough summary of the changes made.

Animation Stuff

Changed buildSequence to use cache as an argument. The sequence_data for name is now defined via a tile's name property. Before it was set based on the sequenceName property. It is possible to lookup an animation name via an object's gid with the getAnimationSequence function. I set the code to play animations by default when the object is created.

TexturePacker Support

Texturepacker support is now successfully enabled. A user has several ways of loading the sprites into Berry.

  1. Place texturepacker sprites and lua files into the tileset_directory for other images
  2. Place texturepacker sprites and lua files into their own directory and provide the texturepacker_dir to Map:new
  3. Load each texturepacker files individually.

For 1 & 2 both the image and lua file names must be the same. (items.png/items.lua, floor.png/floor.lua) If the image name is different from the lua file name for whatever reason, the 3rd option is how you load the files instead. All that is needed is a path for the image file and a path for the lua file. (so you could have items.png for the image sheet and then objects.lua for the code)

Once the sprites are loaded they can be added to the map very easily. Just use map:addSprite and provide the layer name, image name of the sprite, and the x/y coords for it to go. No need to mess around with gid's or tile_ids.

One thing I may look into later is applying map:extend to the texturepacker objects.

Map Caching

I also gave a map it's own cache variable. map.image_cache that stores pretty much all the gid's and names of objects, tiles, animations, and tilesets. This should make the lookup much faster since it can be done directly. Also I removed a lot of the code that was deprecated and dead since there is no longer a need to traverse through all the tilesets to find a matching tile_id. I'm very curious to see what the benchmarks are going to show for speed improvements on map creation and loading. It should be substantial.

Unsolved Math Problems

As I said in the discord, I am having issues translating the x/y position of the texture packer sprite for isometric maps. You may want to take a look at that and see if you can solve it. Also I did notice that when isoToScreen is called in several places, the last argument offset_y is never used. I found that very strange and unusual. The arguments for the function could be further shortened by just passing in map as an argument. Instead of having to use isoToScreen(..., map.tile_width, map.tile_height, ...) every time, you can pass map directly and get the width and height from it in the function.

Minor Code Improvements

Last thing for me to nitpick is that I see you have made some really long ternary operations in the code that I can't make sense of. Generally it's a good idea to encapsulate that into a descriptive local variables that will help both other coders and yourself later on if you come across the same code and forget. So instead of:

if objectgroup and objectgroup.objects and #objectgroup.objects > 0 then
  ...

can be rewritten as:

local has_objects_present = objectgroup and objectgroup.objects and #objectgroup.objects > 0
if has_objects_present then
  ...

Other than that, I'm glad this is finally being closed to finished. Took a lot damn longer than I expected.

@timothymtorres
Copy link
Collaborator Author

locations.zip

These are the files and images I used to profile the code. Unfortunately, there wasn't much of a performance boost. I finally realized why. I had initially assumed that the old version of berry was sorting through every tile for all tilesets in a map. Turns out this was not the case. Instead it filters by using tilset.firstgid > tile.id and if that is the case, then it searches through the tileset for the gid. Instead of sorting through thousands of tiles like I initially thought, turns out it was only sorting through a dozen at a time. So the performance boost is meager. Meh. The code refactor still has other benefits so not totally useless.

@timothymtorres
Copy link
Collaborator Author

timothymtorres commented Feb 14, 2019

texture_pack.zip

This is the texture pack I was testing. Try placing it somewhere in an isometric map via map:addSprite The x/y position is wrong. (although it works fine for orthogonal maps) Relevant code can be found in the addSprite function and in the createObject function around line 988 - 1015

The code is

elseif object.sprite then

	local image_sheet, frame = getImageSheet( map.image_cache, 
											  object.sprite )

	local width, height = getImageSize( map.image_cache, object.sprite )

	image = display.newImageRect( layer, image_sheet, frame, width, height )

	if map.orientation == 'isometric' then

		image.x, image.y = isoToScreen( 
			object.y / map.tile_height, 
			object.x / map.tile_height, 
			map.tile_width, 
			map.tile_height, 
			map.dim.height * map.tile_width * 0.5 
			)
    	image.anchorX, image.anchorY = 0.5, 1   

	elseif map.orientation == 'orthogonal' then 

		image.anchorX, image.anchorY = 0, 1
		image.x, image.y             = object.x, object.y

	end	

ldurniat and others added 9 commits February 15, 2019 22:12
When a sprite is inserted into a layer, it apparently inherits the
position of both the map and layer already.  No need to add the
map/layer x/y positions directly to the sprite x/y.
So this code was used from Simple Tiled Implmentation for Love2Ds
version.  In their code they start counting rows and columns at 1,
instead of Berry which is 0.  Due to this, the layout for all staggered
maps was off slightly.  By checking for the remainder of 1 instead of 0,
this fixes this issue.
@timothymtorres
Copy link
Collaborator Author

Alright it is 100% finished and ready for testing.

@ldurniat ldurniat merged commit 9281913 into ldurniat:master Feb 21, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants