Skip to content

Commit

Permalink
Issue-3645 Add possibility to rotate tiles in tilemap (#6245)
Browse files Browse the repository at this point in the history
* rotate tile

* fix comments

* fix variable

* use combination of `tilemap.H_FLIP`, `tilemap.V_FLIP` and rotation (`tilemap.ROTATE_90`, `tilemap.ROTATE_180`, `tilemap.ROTATE_270`) to get needed tile configuration

* use bitmask instead of 3 bool

* documentation and better constant using

* constants documentation

* add explanation for the bitmask

* rotate physics

* add `rotate90` in proto file

* draw tiles in the editor

* replace tabs with spaces
  • Loading branch information
AGulev committed Dec 27, 2021
1 parent c2ee39c commit 1d18a16
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 52 deletions.
37 changes: 22 additions & 15 deletions editor/src/clj/editor/tile_map.clj
Expand Up @@ -76,22 +76,22 @@

;; manipulating cells

(defrecord Tile [^long x ^long y ^long tile ^boolean h-flip ^boolean v-flip])
(defrecord Tile [^long x ^long y ^long tile ^boolean h-flip ^boolean v-flip ^boolean rotate90])

(defn cell-index ^long [^long x ^long y]
(bit-or (bit-shift-left y Integer/SIZE)
(bit-and x 0xFFFFFFFF)))

(defn paint-cell!
[cell-map x y tile h-flip v-flip]
[cell-map x y tile h-flip v-flip rotate90]
(if tile
(assoc! cell-map (cell-index x y) (->Tile x y tile h-flip v-flip))
(assoc! cell-map (cell-index x y) (->Tile x y tile h-flip v-flip rotate90))
(dissoc! cell-map (cell-index x y))))

(defn make-cell-map
[cells]
(persistent! (reduce (fn [ret {:keys [x y tile h-flip v-flip] :or {h-flip 0 v-flip 0} :as cell}]
(paint-cell! ret x y tile (not= 0 h-flip) (not= 0 v-flip)))
(persistent! (reduce (fn [ret {:keys [x y tile h-flip v-flip rotate90] :or {h-flip 0 v-flip 0} :as cell}]
(paint-cell! ret x y tile (not= 0 h-flip) (not= 0 v-flip) (not= 0 rotate90)))
(transient (int-map/int-map))
cells)))

Expand All @@ -106,14 +106,14 @@
cell-map (transient cell-map)]
(if (< y ey)
(if (< x ex)
(let [{:keys [tile h-flip v-flip]} (first tiles)]
(recur (inc x) y (rest tiles) (paint-cell! cell-map x y tile h-flip v-flip)))
(let [{:keys [tile h-flip v-flip rotate90]} (first tiles)]
(recur (inc x) y (rest tiles) (paint-cell! cell-map x y tile h-flip v-flip rotate90)))
(recur sx (inc y) tiles cell-map))
(persistent! cell-map))))))

(defn make-brush
[tile]
{:width 1 :height 1 :tiles [{:tile tile :h-flip false :v-flip false}]})
{:width 1 :height 1 :tiles [{:tile tile :h-flip false :v-flip false :rotate90 false}]})

(defn make-brush-from-selection
[cell-map start-cell end-cell]
Expand Down Expand Up @@ -239,11 +239,17 @@
u1 (aget uvs (if (.h-flip tile) 0 2))
v1 (aget uvs (if (.v-flip tile) 1 3))]
(recur it
(-> vbuf
(pos-uv-vtx-put! x0 y0 0 u0 v1)
(pos-uv-vtx-put! x0 y1 0 u0 v0)
(pos-uv-vtx-put! x1 y1 0 u1 v0)
(pos-uv-vtx-put! x1 y0 0 u1 v1))
(if (.rotate90 tile)
(-> vbuf
(pos-uv-vtx-put! x0 y1 0 u0 v1)
(pos-uv-vtx-put! x1 y1 0 u0 v0)
(pos-uv-vtx-put! x1 y0 0 u1 v0)
(pos-uv-vtx-put! x0 y0 0 u1 v1))
(-> vbuf
(pos-uv-vtx-put! x0 y0 0 u0 v1)
(pos-uv-vtx-put! x0 y1 0 u0 v0)
(pos-uv-vtx-put! x1 y1 0 u1 v0)
(pos-uv-vtx-put! x1 y0 0 u1 v1)))
(min-l min-x x0)
(min-l min-y y0)
(max-l max-x x1)
Expand Down Expand Up @@ -297,12 +303,13 @@
{:id id
:z z
:is-visible (if visible 1 0)
:cell (mapv (fn [{:keys [x y tile v-flip h-flip]}]
:cell (mapv (fn [{:keys [x y tile v-flip h-flip rotate90]}]
{:x x
:y y
:tile tile
:v-flip (if v-flip 1 0)
:h-flip (if h-flip 1 0)})
:h-flip (if h-flip 1 0)
:rotate90 (if rotate90 1 0)})
(vals cell-map))})

(g/defnode LayerNode
Expand Down
1 change: 1 addition & 0 deletions engine/gamesys/proto/gamesys/physics_ddf.proto
Expand Up @@ -328,6 +328,7 @@ message SetGridShapeHull
required uint32 hull = 4;
required uint32 flip_horizontal = 5;
required uint32 flip_vertical = 6;
required uint32 rotate90 = 7;
}

// System message (TileGrid=>CollisionObject)
Expand Down
1 change: 1 addition & 0 deletions engine/gamesys/proto/gamesys/tile_ddf.proto
Expand Up @@ -80,6 +80,7 @@ message TileCell
required uint32 tile = 3 [default = 0];
optional uint32 h_flip = 4 [default = 0];
optional uint32 v_flip = 5 [default = 0];
optional uint32 rotate90 = 6 [default = 0];
}

message TileLayer
Expand Down
Expand Up @@ -1120,6 +1120,7 @@ namespace dmGameSystem
dmPhysics::HullFlags flags;
flags.m_FlipHorizontal = ddf->m_FlipHorizontal;
flags.m_FlipVertical = ddf->m_FlipVertical;
flags.m_Rotate90 = ddf->m_Rotate90;
dmPhysics::SetGridShapeHull(component->m_Object2D, ddf->m_Shape, row, column, hull, flags);
uint16_t child = column + tile_grid_resource->m_ColumnCount * row;
uint16_t group = 0;
Expand Down
51 changes: 31 additions & 20 deletions engine/gamesys/src/gamesys/components/comp_tilegrid.cpp
Expand Up @@ -60,9 +60,8 @@ namespace dmGameSystem
{
struct Flags
{
uint16_t m_FlipHorizontal : 1;
uint16_t m_FlipVertical : 1;
uint16_t : 14;
uint8_t m_TransformMask : 3;
uint8_t : 5;
};

TileGridComponent()
Expand Down Expand Up @@ -223,15 +222,14 @@ namespace dmGameSystem
region->m_Dirty = 1;
}

void SetTileGridTile(TileGridComponent* component, uint32_t layer, int32_t cell_x, int32_t cell_y, uint32_t tile, bool flip_h, bool flip_v)
void SetTileGridTile(TileGridComponent* component, uint32_t layer, int32_t cell_x, int32_t cell_y, uint32_t tile, uint8_t transform_mask)
{
TileGridResource* resource = component->m_Resource;
uint32_t cell_index = CalculateCellIndex(layer, cell_x, cell_y, resource->m_ColumnCount, resource->m_RowCount);
component->m_Cells[cell_index] = tile;

TileGridComponent::Flags* flags = &component->m_CellFlags[cell_index];
flags->m_FlipHorizontal = flip_h;
flags->m_FlipVertical = flip_v;
flags->m_TransformMask = transform_mask;

SetRegionDirty(component, cell_x, cell_y);
}
Expand Down Expand Up @@ -369,8 +367,19 @@ namespace dmGameSystem
component->m_Cells[cell_index] = (uint16_t)cell->m_Tile;

TileGridComponent::Flags* flags = &component->m_CellFlags[cell_index];
flags->m_FlipHorizontal = cell->m_HFlip;
flags->m_FlipVertical = cell->m_VFlip;
flags->m_TransformMask = 0;
if (cell->m_HFlip)
{
flags->m_TransformMask = FLIP_HORIZONTAL;
}
if (cell->m_VFlip)
{
flags->m_TransformMask |= FLIP_VERTICAL;
}
if (cell->m_Rotate90)
{
flags->m_TransformMask |= ROTATE_90;
}
}
}

Expand Down Expand Up @@ -507,11 +516,22 @@ namespace dmGameSystem
TileGridVertex* CreateVertexData(TileGridWorld* world, TileGridVertex* where, TextureSetResource* texture_set, dmRender::RenderListEntry* buf, uint32_t* begin, uint32_t* end)
{
DM_PROFILE(TileGrid, "CreateVertexData");
/*
* 0----3
* | \ |
* | \ |
* 1____2
*/
static int tex_coord_order[] = {
0,1,2,2,3,0,
3,2,1,1,0,3, //h
1,0,3,3,2,1, //v
2,3,0,0,1,2 //hv
2,3,0,0,1,2, //hv
// rotate 90 degrees:
3,0,1,1,2,3,
0,3,2,2,1,0, //h
2,1,0,0,3,2, //v
1,2,3,3,0,1 //hv
};

dmGameSystemDDF::TextureSet* texture_set_ddf = texture_set->m_TextureSet;
Expand Down Expand Up @@ -561,18 +581,9 @@ namespace dmGameSystem
float p[4];
CalculateCellBounds(x, y, 1, 1, p);
const float* puv = &tex_coords[tile * 8];
uint32_t flip_flag = 0;

TileGridComponent::Flags flags = component->m_CellFlags[cell];
if (flags.m_FlipHorizontal)
{
flip_flag = 1;
}
if (flags.m_FlipVertical)
{
flip_flag |= 2;
}
const int* tex_lookup = &tex_coord_order[flip_flag * 6];
const int* tex_lookup = &tex_coord_order[flags.m_TransformMask * 6];

#define SET_VERTEX(_I, _X, _Y, _Z, _U, _V) \
{ \
Expand Down Expand Up @@ -856,7 +867,7 @@ namespace dmGameSystem
*/
uint32_t tile = st->m_Tile - 1;

SetTileGridTile(component, layer_index, cell_x, cell_y, tile, false, false);
SetTileGridTile(component, layer_index, cell_x, cell_y, tile, 0);

// Broadcast to any collision object components
// TODO Filter broadcast to only collision objects
Expand Down
10 changes: 9 additions & 1 deletion engine/gamesys/src/gamesys/components/comp_tilegrid.h
Expand Up @@ -55,11 +55,19 @@ namespace dmGameSystem

uint16_t GetTileGridTile(const TileGridComponent* component, uint32_t layer, int32_t cell_x, int32_t cell_y);

void SetTileGridTile(TileGridComponent* component, uint32_t layer, int32_t cell_x, int32_t cell_y, uint32_t tile, bool flip_h, bool flip_v);
void SetTileGridTile(TileGridComponent* component, uint32_t layer, int32_t cell_x, int32_t cell_y, uint32_t tile, uint8_t transform_mask);

uint16_t GetTileCount(const TileGridComponent* component);

void SetLayerVisible(TileGridComponent* component, uint32_t layer, bool visible);

enum TileTransformMask
{
FLIP_HORIZONTAL = 1,
FLIP_VERTICAL = 2,
ROTATE_90 = 4
};
static uint8_t MAX_TRANSFORM_FLAG = FLIP_HORIZONTAL + FLIP_VERTICAL + ROTATE_90;
}

#endif
114 changes: 106 additions & 8 deletions engine/gamesys/src/gamesys/scripts/script_tilemap.cpp
Expand Up @@ -190,19 +190,28 @@ namespace dmGameSystem
* That is, it is not possible to extend the size of a tile map by setting tiles outside the edges.
* To clear a tile, set the tile to number 0. Which tile map and layer to manipulate is identified by the URL and the layer name parameters.
*
* Transform bitmask is arithmetic sum of one or both FLIP constants (`tilemap.H_FLIP`, `tilemap.V_FLIP`) and/or one of ROTATION constants
* (`tilemap.ROTATE_90`, `tilemap.ROTATE_180`, `tilemap.ROTATE_270`).
* Flip always applies before rotation (clockwise).
*
* @name tilemap.set_tile
* @param url [type:string|hash|url] the tile map
* @param layer [type:string|hash] name of the layer for the tile
* @param x [type:number] x-coordinate of the tile
* @param y [type:number] y-coordinate of the tile
* @param tile [type:number] index of new tile to set. 0 resets the cell
* @param [h-flipped] [type:boolean] optional if the tile should be horizontally flipped
* @param [v-flipped] [type:boolean] optional i the tile should be vertically flipped
* @param [transform-bitmask] [type:number] optional flip and/or rotation should be applied to the tile
* @examples
*
* ```lua
* -- Clear the tile under the player.
* tilemap.set_tile("/level#tilemap", "foreground", self.player_x, self.player_y, 0)
*
* -- Set tile with different combination of flip and rotation
* tilemap.set_tile("#tilemap", "layer1", x, y, 0, tilemap.H_FLIP + tilemap.V_FLIP + tilemap.ROTATE_90)
* tilemap.set_tile("#tilemap", "layer1", x, y, 0, tilemap.H_FLIP + tilemap.ROTATE_270)
* tilemap.set_tile("#tilemap", "layer1", x, y, 0, tilemap.V_FLIP + tilemap.H_FLIP)
* tilemap.set_tile("#tilemap", "layer1", x, y, 0, tilemap.ROTATE_180)
* ```
*/
static int TileMap_SetTile(lua_State* L)
Expand Down Expand Up @@ -263,10 +272,32 @@ namespace dmGameSystem
assert(top + 1 == lua_gettop(L));
return 1;
}

bool flip_h = lua_toboolean(L, 6);
bool flip_v = lua_toboolean(L, 7);
SetTileGridTile(component, layer_index, cell_x, cell_y, tile, flip_h, flip_v);
uint8_t bitmask = 0;
if (lua_isnumber(L, 6) && top == 6)
{
// Read more info about bitmask and constants values in SETCONSTANT macros
bitmask = dmMath::Abs(luaL_checkinteger(L, 6));
if (bitmask > MAX_TRANSFORM_FLAG)
{
return luaL_error(L, "tilemap.set_tile called with wrong tranformation bitmask (tile: %d)", lua_tile);
}
}
else
{
// deprecated API flow with boolean flags
bool flip_h = lua_toboolean(L, 6);
bool flip_v = lua_toboolean(L, 7);
if (flip_h)
{
bitmask = FLIP_HORIZONTAL;
}
if (flip_v)
{
bitmask |= FLIP_VERTICAL;
}
}

SetTileGridTile(component, layer_index, cell_x, cell_y, tile, bitmask);

dmMessage::URL sender;
if (dmScript::GetURL(L, &sender))
Expand All @@ -278,8 +309,9 @@ namespace dmGameSystem
set_hull_ddf.m_Column = cell_x;
set_hull_ddf.m_Row = cell_y;
set_hull_ddf.m_Hull = tile;
set_hull_ddf.m_FlipHorizontal = flip_h;
set_hull_ddf.m_FlipVertical = flip_v;
set_hull_ddf.m_FlipHorizontal = (bitmask & FLIP_HORIZONTAL) > 0 ? 1 : 0;
set_hull_ddf.m_FlipVertical = (bitmask & FLIP_VERTICAL) > 0 ? 1 : 0;
set_hull_ddf.m_Rotate90 = (bitmask & ROTATE_90) > 0 ? 1 : 0;
dmhash_t message_id = dmPhysicsDDF::SetGridShapeHull::m_DDFDescriptor->m_NameHash;
uintptr_t descriptor = (uintptr_t)dmPhysicsDDF::SetGridShapeHull::m_DDFDescriptor;
uint32_t data_size = sizeof(dmPhysicsDDF::SetGridShapeHull);
Expand Down Expand Up @@ -482,10 +514,76 @@ namespace dmGameSystem
{0, 0}
};

/*# flip tile horizontally
*
* @name tilemap.H_FLIP
* @variable
*/
/*# flip tile vertically
*
* @name tilemap.V_FLIP
* @variable
*/
/*# rotate tile 90 degrees clockwise
*
* @name tilemap.ROTATE_90
* @variable
*/
/*# rotate tile 180 degrees clockwise
*
* @name tilemap.ROTATE_180
* @variable
*/
/*# rotate tile 270 degrees clockwise
*
* @name tilemap.ROTATE_270
* @variable
*/

void ScriptTileMapRegister(const ScriptLibContext& context)
{
lua_State* L = context.m_LuaState;
DM_LUA_STACK_CHECK(L, 0);
luaL_register(L, "tilemap", TILEMAP_FUNCTIONS);

#define SETCONSTANT(name, val) \
lua_pushnumber(L, (lua_Number) val); \
lua_setfield(L, -2, #name);\

SETCONSTANT(H_FLIP, FLIP_HORIZONTAL);
SETCONSTANT(V_FLIP, FLIP_VERTICAL);
SETCONSTANT(ROTATE_90, ROTATE_90);
SETCONSTANT(ROTATE_180, -(FLIP_HORIZONTAL + FLIP_VERTICAL));
SETCONSTANT(ROTATE_270, -(FLIP_HORIZONTAL + FLIP_VERTICAL + ROTATE_90));

/* It's enough to have 3 flags to specify a quad transform. 1st bit for h_flip, 2nd for v_flip and 3rd for 90 degree rotation.
* All the other rotations are possible to specify using these 3 bits.
* For example, rotating a quad 180 degrees is the same as flip it horizontally AND vertically.
* Here is a transforms correspondence table:
* |----------------------------------------------
* |val| bitmask| basic 3bits | corresponding |
* | | | transforms | transformations |
* |----------------------------------------------
* | 0 | (000) | R_0 | R_180 + H + V |
* | 1 | (001) | H + R_0 | R_180 + V |
* | 2 | (010) | V + R_0 | R_180 + H |
* | 3 | (011) | V + H + R_0 | R_180 |
* | 4 | (100) | R_90 | R_270 + H + V |
* | 5 | (101) | H + R_90 | R_270 + V |
* | 6 | (110) | V + R_90 | R_270 + H |
* | 7 | (111) | V + H + R_90 | R_270 |
* -----------------------------------------------
*
* Since we want to use arithmetic sum in Lua API (and avoid using of bit module)
* and also want to avoid extra arithmetic operations in the engine (because we are doing them on Lua side anyways)
* we can use mirrored values from basic transforms
* R_180 = -(V + H + R_0) = -3
* R_270 = -(V + H + R_90) = -7
* To make sure that the final bitmask is equal to the original one we should use Math::Abs() function on it.
*/

#undef SETCONSTANT

lua_pop(L, 1);
}
}

0 comments on commit 1d18a16

Please sign in to comment.