craftyjs / Crafty

JavaScript Game Engine
http://craftyjs.com
MIT License
3.42k stars 557 forks source link

Proposed API for the DiamondIso component #917

Open airza opened 9 years ago

airza commented 9 years ago

I'm adding more functionality and would like to propose a public API for this to make sure I'm not missing anything or doing it inefficiently. This will also help the docs, and make sure I'm writing the correct unit tests.

init: function (tw, th, mw, mh, x, y) Creates the grid. Tile width and height are specified by the first two parameters. The map's width and height are specified by the second two, with the X axis being down-right and Y axis being down-left. The X and Y coordinate indicate the map's placement on the canvas, such that the top of the tile at (0,0,0) on the grid has its top corner here. Right now

place: function(obj, x, y, layer) Puts an object (obj) into the grid at X Y and grid height layer. If the object's height or width isn't a multiple of the grid height this probably won't work. There's no need for a sprite or whatever to actually fill the entire grid, but its actual crafty size has to fit correctly in. We should probably have a switch that will determine if the grid updates that object's realX/realY position as well. Destroying an object also removes it from this grid.

It's not clear to me from a design perspective if tiles should have their current grid location passed back to them by this function- is it a more maintainable architecture if all of the code that can tell objects about the relative location of other objects lives inside of the grid object itsself? I really have no idea.

detachTile: function(obj) Removes an object from the grid's storage and returns it to the user.

centerAt:(x, y) This function currently centers the viewpoint on a given tile (Supposedly). Since sometimes one probably wants units to be centered on a given tile this one makes sense to keep in (I don't think the centering is exactly perfect though, i should fix that.

getZAtLoc:(x,y,layer): Used both internally and externally- the map sets up certain Z values so that tiles render correctly on top of one another. This may be possible to make private, but right now entities that walk on top of the map use it to get Z values for where they should be walking.

getOverlappingTiles: I don't remember why I wrote this method. LOL. It currently loops through the entire grid to figure out all of the entities which currently are under one another on the board, which is probably useful if you have drag and drop functionality. It seems like Crafty.findClosestEntityByComponent is a much better choice for anything you could use a grid for, so I'm going to remove this.

polygon: This looks like it's trying to do something with the bounding box of elements, but it's not correct and it's not clear why the grid should handle drawing the areamap of a tile. I'm going to delete it.

getTile(x,y,z): Gets the tile at x,y, and layer Z. Pretty straightforward. Since we may want to turn this into a sparse representation internally someday this is going to be a getter.

getTileDimensions(): Returns the tile height and width (Which right now have to be the same)

findPath(Start, End): This i've already implemented - when given a start/end X and Y coordinate, it will look at things on the grid with the component "Obstacle" and find a way around it. It would be nice to work with the collision library on this somehow, but collision on the grid and on a 2d screen work two fundamentally different ways. This only currently works for things on Z level 1 that are walking around on top of Z level 0. It returns a list of squares on the grid that the entity should walk through.

Trivially easy upgrades for this would include: A parameter to specify additional components that represent obstacles, and a parameter to allow diagonal movement. (Right now the advantage of having the "obstacle" component is that the grid can store the collision map and then call it up when an entity walks around.) Movement fails for squares on the grid with an obstacle at level 1 or that are empty at level 0.

moveEntity(startCoords/object, destination,speed): Moves an entity on the map using the most efficient route available. Always walks through the middle of each tile (it's not difficult to change this or allow a specified path.) Currently it updates the entity's placement in the grid when it reaches the center of the square, but probably it should update as soon as the entity moves in. It's also a method attached to its own component since i haven't gotten brave enough to try adding new components to the library.

pos2px(left,top,layer): This returns the x/y coordinate of the tile at this location (IE, where the x and y of a tile would have to be placed in order to be correctly placed at this X Y Z.) Layer isn't implemented yet, but is a simple addition of tile height to the Y coordinate.

px2Pos(x,y): This returns which tile on layer zero the specified coordinates are on. This could theoretically be updated to specify different layers, but right now I don't see a super obvious reason to do so.

The remaining functionality that i think needs to be added: rotate(Clockwise/Counterclockwise): Rotates the grid left or right, in place, so that everything is in every other position. Triggers "GridRotate" on every single entity in the grid, passing a translation function that correctly translates realX/realY/z coordinates. (I think this is a simple linear transform, and hopefully not many entities on the grid need to bind to this.) The X/Y coordinates of the grid probably shouldn't rotate unless there is a compelling reason for them to do so. We COULD make every single function which interfaces between pixels and grid locations dynamically change based on rotation, but this would be a hellscape.

The current entities which I am developing/would like to develop in conjunction with this proposed functionality:

"Obstacle": It counts as something you can't walk through (No code needed for this, it just helps performance for pathfinding if the grid collision map isn't dynamically updated for every single pathfinding call.)

"PathFollower": Currently has one function: followWaypoints(Array[Array] waypoints,speed): It follows a list of realx/realy coordinates at the given speed using the tween component. Calls "Waypoint" at each one and "WaypointsDone" at the end. Needed for grid movement! Also handy for objects that want to move in set paths on the map, though.

Does anyone have any questions or comments about this proposed API? It will come with [drumroll] updated documentation for the diamondIso component, and should allow us to remove the regular iso (I'm not clear if anyone is using the other one and its component system is kind of unintuitive.)

airza commented 9 years ago

Oh, I just forgot that every entity will pretty much need to bind to grid rotate because entities will have different sprites based on rotations. Womp womp.

airza commented 9 years ago

So, maybe it's a better idea to have a specified GridEntity component that will know its own position, and have a function available to replace its sprite when rotation happens.

starwed commented 9 years ago

I'll try to take a detailed look at this later, but one approach would be to make everything much more general. Maybe things like "GridEntity" should be abstracted out, and then you can specify a method for translating grid coordinates to screen coordinates. That way you could use the same methods regardless of whether you were making an isometric or a top down game.

I guess a good test of an isometric engine's flexibility would be if you could implement something crazy like naya's quest. :)

airza commented 9 years ago

Well, I finished everything except rotation, which is a horrendous pain in the ass. It might be a good idea to standardize the input and output point objects, but I don't remember if it breaks backwards compatibility to do so.

starwed commented 9 years ago

If you want some specific feedback, feel free to open a PR with what you've got -- no worries if you plan on doing more work or cleaning it up, you can just update the PR as you do so.

mucaho commented 9 years ago

I think isometric is a good motivating example to introduce the concept of cameras.

This can be done before / parallel to #712, mpetrovic's layer branch also contains a camera implementation. In my opinion that should be our main focus until next release (post 0.7). I will try and make a small demo to see if it's doable.

airza commented 9 years ago

I dropped a PR targeting the previous implementation. A camera would be nice to have but the assumption that all isometric sprites can be freely rotated (or even viewed from a skewed azimuth like an iso camera would use) and still look correct seems wrong.

mucaho commented 9 years ago

the assumption that all isometric sprites can be freely rotated (or even viewed from a skewed azimuth like an iso camera would use) and still look correct seems wrong

Yes, and the more I think about it, the less appealing orthographic cameras with 3d position and rotation seem to me. Possible performance impact and browser compatibility limitations just for isometric perspective.

My major motivation behind isometric camera is to be able to run game logic on a node back-end and feed back position updates to a client front-end and have everything correctly displayed. No conversions needed, existing components (like Fourway) work out-of-the-box.

I have been toying around with an alternative idea lately: You have 2 sets of entities; one set of hidden entities driving the game logic, which feed their constantly changing state (like position) to the set of isometric entities. This way we should be able leverage the work & effort that has been put into the current implementation, while also being compatible to existing functionality (like Fourway).

Playing around with current isometric implementation, I have encountered an issue. @airza Would you kindly try to run this example on your current implementation? Do you also notice the stone box making sudden, periodic jumps?

airza commented 9 years ago

Hmm... That's not good. Let me take a look at it while I think about the idea that you're suggesting. :)

airza commented 9 years ago

Ah, I see:

The isometric implementation uses a different addressing system for tiles on the grid than diamond iso. It tries to build the px2pos against a grid where the tiles on the same X axis are placed in a zig-zagging left-to-right pattern. The author of this implementation attempts to distinguish between these by using a bitwise AND:

left: x * this._tile.width + (y & 1) * (this._tile.width / 2)

When the float jumps from under 3 to over 3, the y&1 suddenly snaps from 0 to 1 which causes it to jump quite a bit- The author of that didn't anticipate that people would try to use it to interpolate values between two tiles. I can see how you'd fix it (calculate the position of the tile on the left and right of it and then draw a vector between, but the difficulty of that coordinate system vs diamondIso is why I chose to update diamondiso in the first place, so it's a different issue than this one.

mucaho commented 9 years ago

So it's not an issue at all, that's working as intended according to the zig-zag isometric ordering. After I switched to diamond ordering the box moved in a straight line as expected, thanks for taking a look at it.

mucaho commented 9 years ago

Yes, it may be feasable Has:

Misses:

airza commented 9 years ago

Yeah, this is pretty similar to what I have in place for the game i've been working on using this library. Though for me it was only important to figure out when characters cross a boundary since all of the movement is mouse-bound.

If we want an entity which is designed to move across the surface of isos it could be done with the built in collision map from the PR. The Z layer stuff is fixed on that one too.

On Tue, Jul 21, 2015 at 1:24 PM, mucaho notifications@github.com wrote:

Yes, it may be feasable https://jsfiddle.net/gt9o6469/ - lacks correct depth ordering, resizing and rotation.

— Reply to this email directly or view it on GitHub https://github.com/craftyjs/Crafty/issues/917#issuecomment-123426547.

mucaho commented 9 years ago

This collision map sounds interesting. Is it functionally equivalent to the collision map you would have from bird/top-down perspective? Or does it recognize that the player can't walk on tiles that are hidden behind a tall object? e.g. In Secret of Mana you can't walk behind a house because it would obstruct your vision - this is maybe a tad bad example because it's not isometric :) Secret of Mana house isometric

airza commented 9 years ago

Yeah, it's the latter type - when you place or remove a tile from the grid on the 1 or 0 z layer it checks to see if 1)the tile on the 0 z layer is in place (IE, there's a floor) and 2) there's no tile with the "Obstacle" component on the z layer 1.

I don't have any linkups in place with the existing collision map since I did things with the mouse - you click on the tile and then it uses the pathfinding at the grid level to see which boxes are non-crossible If I was trying to leverage the existing collision functionality for this, it seems like having a collision box projected onto the surface of the board for solid tiles would be the way to go. Then entities walking could have a subcomponent where their feet were that invisibly checked collision.

mucaho commented 9 years ago

Ok, let me make sure I understood everything correctly. Isometric, as it currently is implemented in the linked PR, includes:

What I would ideally prefer to see eventually:

How I propose we proceed:

Thoughts? This is probably a big design decision, so I hope other people voice their opinion also. @starwed @kevinsimper (sry for all the cc's lately)

airza commented 9 years ago

It has collision boundaries at the discrete level- you can ask if it if a particular X,Y coordinate is enterable by an entity travelling on top of the grid (@ z level 1).

I haven't tested centerAt much, but I'll go doublecheck it right now. TBH i'm surprised that it's a method of the grid and there isn't just a Crafty.Viewport.Centeron(o) which calculates the center of the object passed and the viewport and aligns accordingly.

"Utility methods to convert a 3D point from birds-eye view to isometric view and vice versa"- yeah, it does the vectorization conversion automatically.

If you want to take a look at what i'm using it for (which showcases some of the nice features in a way that the base library doesn't have right now) you can look here:

https://aqueous-fjord-6193.herokuapp.com/Crafty

You can click around and the character will route intelligently. If you so desire, you can use the bright green button to switch to a mode where the obstacles can be dragged around- then she will correctly route around them in the most effective way possible.

The actual walking code is custom and basically just maps the array of coordinates that it receives from the pathfinding.

airza commented 9 years ago

The intended behavior of centerAt is to center the camera on the grid, right?

mucaho commented 9 years ago

Nice looking game you got there!

The intended behavior of centerAt is to center the camera on the grid, right?

Yes and as you said all viewport related functions should work out of the box with isometric entities. I guess this isometric-specific centerAt also considers tile.width in order to properly center the entity on screen. I wonder if there is a noticeable difference between iso centerAt and Crafty.viewport.centerAt