• Edited

xRegnarokx What is z index you're referring to? In 3d, you don't need any z index. You can simply have continuous elevations.

You should first describe your map. How is each cell defined via its logical properties (terrain, elevation etc) and then how you plan to represent that visually. Then we can discuss how to reflect that in your custom grid structure and how it can all be turned into movement in 3d space.

Or better yet, give a reference to an existing game that's similar to what you're trying to do.

    xyz Okay, I guess I am not understanding what you mean by continuous elevations. If you have a cell at z index 0 and one at 2 how would you handle if you wanted jump off the ledge to get to the 0 tile, or if you have stair tiles that halfway up would be at z index 1.

    I guess I am inspired a bit by an mmorpg called Tibia, at least it gave me the idea for the perspective. I believe is is 2D but with a perspective that gives it more depth. Also, in it you can climb on stacked objects to climb onto ledges.

    • xyz replied to this.
      • Edited

      xRegnarokx Oh so there's vertical movement as well? You should have mentioned that from the start. Best to post some gameplay/movement footage.

      I googled Tibia gameplay videos. All movement there appears to be happening on the floor plane.

        xyz Yeah, it is since it is 2D there isn't an actual z axis in tibia, they simulate it though where items can get stacked, and players can climb onto stacked items.

        Here in the first few seconds you see someone standing on a barrel in the bottom left in the building. I know in 2D this is smoke and mirrors, but I wasn't able to figure out the perspective challenges in godots 2D engine. The sorting characters behind objects both on the x and y axis.
        [

        So, I want to try and emulate that in 3d, but that is granted more difficult. But, I am confident I can figure out what I need if I can find a way to get the z index of the top of terrain. Objects are easy, but the tiles on the gridmap are harder.

        • xyz replied to this.
          • Edited

          xRegnarokx You should start by modeling your map with data. Introduce a cell class that contains relevant cell data

          class Cell:
          	var z: int = 0 # how many stacks, your "z index"
          	var walkable: bool = true
          	# add more stuff later

          Have a "2D" array of such cells that represents your map:

          var map: Array[Cell]

          The array is 1D (since there are no multidimensional arrays in GDScript) but you can implement 2d access functions so it'll work like a 2D array/map:

          func get_map_cell(map_coord: Vector2i) -> Cell:
          	# stuff
          
          func set_map_cell(map_coord: Vector2i, cell: Cell) -> void:
          	# stuff

          That's your basic map data structure. You can do a lot with it.

            xyz I am slightly confused is this for implementing with godot in 3D or in 2D?

            Because if this is 2D I am unsure how this would help with my original problem that caused you to suggest I move to 3D. Which was I can y-sort for objects that are "above" however there isn't a good way of handling when you are suppose to be in front of the object on the x-axis but are still above it on the y-axis.

            If it is 3D, I still am unsure if I understand how this would help me with finding the z index for movement purposes. How is my character if I am not using the build in physics to know where they should be on the z index.

            I could use the gridmap get_used_cell_by_item() use the item id to manually via code set each items relative z index. Or, I could just create two gridmaps for the two heights of 1 meter and 2 meter.

            Example:

            Class Cell:
                 var elevation : int #sets the items height based on 2 meter increments, each 2 meters is a new elevation
                 var z_index : int # this would be set at either 0 or 1

            So, at elevation 0 there would be only tiles at 0 height and 1 meter. So, 0 would be what you'd walk on, and then there would be terrain you could climb on which would be 1 meter. Your player would store his relative elevation, and whenever he moved up 2 meters his elevation would change, so thus while navigating and checking cells based on coordinates they'd all be searched based on their z index at their elevation, but then when going to move to them it would be based on their var z_index to know if you can walk normal, or need to hop up on them.

            Do you think this would be a logical way to go about this? I would just have to make sure that all blocks that can be walked on are exactly 1 or 2 meters high.

            • xyz replied to this.
              • Edited

              xRegnarokx It can be either. Your map has certain characteristics that should be described through data structures. This is called modeling with data and is essential for developing any kind of software system. I tried to do this in a most rudimentary (and incomplete) form, following your (incomplete) description and what is shown in the video. This is the part that is separate from actual visual representation which can be either 2d or 3d. For this part of code, looks don't matter. When you model your game world with data, you need to primarily think in this semi-abstract way, which doesn't really have anything to do with 2d or 3d or Godot for that matter. It only has to do with your game world. Looks are built on top of it, or based on it.

              • Edited

              xRegnarokx If it is 3D, I still am unsure if I understand how this would help me with finding the z index for movement purposes. How is my character if I am not using the build in physics to know where they should be on the z index.

              There's literally z property in the cell data which holds the integer number of stacks. If you know the height of one stack, multiply z with that height and you've got the vertical position in the world. It's basically a simple version of heightfield.

              If you compare z of the current cell and z of the cell you want to go to, you'll know if you need to climb, descend or go straight. Or if a cell is an obstacle that is not climbable etc.. All this can and should determine how you animate the traversal.

                xyz Ahh okay, so you'd set the z based on its height then you'd be able to find it using Vector2i, which would just ignore the z index. I think I was confused since you had Vector2i, I wasn't thinking about the fact you could set up a grid like that, where the z index is stored separately to enable reliable cell discovery. I am still wrapping my mind around doing things in 3D and coming from 2D the z has been throwing me off.

                Thank you so much! You have always pointed me in a good direction. I'll leave this open until I can create some test code to see if I am able to utilize these principles.

                • xyz replied to this.
                  • Edited

                  xRegnarokx Note that z is perhaps a wrong name to use here. Height would be much more appropriate as it's not linked to any coordinate system. In Godot's 3d the convention is that a ground plane is in x and z directions and up is in y direction.

                  Here's a more elaborate "sketch" of how the whole map class could look like. Don't just copy paste this. Might not work. It's just to show the way of thinking/organizing.

                  class_name Map extends RefCounted
                  
                  # cell data struct
                  class Cell:
                  	var terrain: String = "grass"
                  	var height: int = 0
                  	var walkable: bool = true
                  	func _init(iterrain: String, iheight: int, iwalakble: bool = true): # object constructor
                  		terrain = iterrain
                  		height = iheight
                  		walkable = iwalakble
                  	
                  # map size (number of cells)
                  var _size: Vector2i
                  
                  # map
                  var _cells: Array[Cell]
                  
                  # pyisical cell size
                  var _cell_size_physical: float = 1.0 # xz size of a cell in world units
                  var _cell_height_pyhsical: float = 1.0 # height of a cell in world units
                  
                  # initialize the size of array that holds cell data
                  func initialize(size: Vector2i):
                  	_size = size
                  	_cells.resize(_size.x * _size.y)
                  	
                  # returns 1D array index from 2D cell coords, depending on map size 
                  # used to access a 1D array as if it was a 2D array
                  func _get_array_index(pos: Vector2i) -> int:
                  	assert(pos.x >= 0 and pos.x < _size.x and pos.y >= 0 and pos.y < _size.y) # boundary check
                  	return pos.x + pos.y * _size.y
                  	
                  func get_cell(pos: Vector2i) -> Cell:
                  	return _cells[_get_array_index(pos)]
                  	
                  func set_cell(pos: Vector2i, data: Cell) -> void:
                  	var cell = get_cell(pos)
                  	cell.terrain = data.terrain
                  	cell.height = data.height
                  	cell.walkable = data.walkable
                  	
                  # some test utility function
                  func add_wall(pos: Vector2i) -> void:
                  	set_cell(pos, Cell.new("wall", 1, false))
                  	
                  func add_barrel(pos: Vector2i) -> void:
                  	set_cell(pos, Cell.new("barral", 1, true))
                  	
                  # etc...

                  Now you can use some generation algorithm to generate an actual maze or map layout into this structure. And when you have that, you can proceed to make a visual representation of it in either 2d using fake ortho perspective or in 3d using actual geometry.

                  If you prefer visual editing of the map, You can make an actual GridMap with some tiles etc... and write an utility script that interprets what's in the GridMap and populates your data structure.

                  Alternatively you could implement the same data structure as an extension of a GridMap node, making it more coupled with actual 3D graphics, but the way of thinking about and organizing map data would be exactly the same. You need a "logical" structure that's "underneath" the visual representation.

                  The movement comes after this, because the movement system needs this for proper functioning. You can't really have a grid based movement system without some data in a grid 😃

                    xyz Awesome, I appreciate the example. I try not to copy and paste, unless there is something I'm using as a template and am planning on rewriting anyway. I find writing the code myself helps me understand better what is going on.

                    This makes sense, so, if I do the visual design -> data structure I will create a system to translate the visual fluff to actual useable information in the map data.

                    So, since I want to go visual design -> map data route, I also need to think through how I am going to translate the gridmap visuals into the cell/map data.

                    xyz Could I use metadata for setting certain data of my cells? Such as setting metadata of base height of a tile. Or unpassable, or a myriad of different things that could then be grabbed while creating the cell classes. Would this work, or is this a bad idea/practice?

                    • xyz replied to this.
                      • Edited

                      xRegnarokx Technically you could store all cell data into metadata of the gridmap meshes. In that case you wouldn't need a custom Cell class or an array of cells, although the code that access the cell data would be a bit more verbose.

                        xyz Yeah, I realized that as I was running into that issue when I was looking at how that would work.

                        Also, I noticed you used assert() inside of _get_array_index(). What is this used for (boundry checks obviously)? Just reading its description it sounds like it only used for debugging purposes, and doesn't do anything when exported? Or am I not understanding its description in the documentation?

                        Also, I noticed in the assertion that you were checking that there wasn't negative value x and y coords? Would you then recommend that when I am setting up my cells that I keep them increasing from 0,0? So, my absolute top left cell would be 0,0 and then I would go from there?

                        • xyz replied to this.
                          • Edited

                          xRegnarokx assert() there is just a hint that boundary check should be implemented here. You can do it in any way that fits your system.

                          Cell coordinates can be in any range although starting at 0,0 may make code simpler. Again, this is implementation detail that depends on characteristics of your system. The decision on how exactly to do it is yours to make.

                          • Edited

                          xRegnarokx The important thing to understand here is how this will facilitate movement handling.

                          In a grid based system, a character/entity can occupy only one cell, and it must occupy exactly one cell at any given time. There's no transitional state in such a system. All movement happens instantly. When a character is moved, it immediately should hop from one cell to another. Smooth animation of movement from cell to cell is just a visual effect that happens after the underlying state has already been changed.

                          Knowing this you should first implement the instant movement. When a command is pressed, check if the movement from the current tile to the next tile is possible (by looking at the cell data and applying movement rules). If yes - change the underlying state and update the visual representation to reflect that state.

                          Upgrading the system to smooth transitional animations is then just a matter of interpolation/animation and preventing further movement until the current animation is finalized.

                            xyz Hey, thanks for that! Also, that makes sense with assert, I'll work on making a boundry checker to my current code. I feel like maybe I'm going over complicated with what I am doing, maybe I'm missing something. I have still some bugs I'm ironing out, here is what I have so far, it is more to try and judge if there are any critical flaws in my thinking as I am implementing this data structure and converting the visual to be translated into the data structure.

                            extends GridMap
                            
                            class Cell:
                            	var height: int
                            	var walkable: bool
                            	func _init(iheight: int, iwalkable: int) -> void:
                            		height = iheight
                            		walkable = iwalkable
                            
                            # map size (cell number)
                            var _size: Vector2i
                            
                            # map (array of cells)
                            var _cells: Array[Cell]
                            
                            # cell size in world units (meter)
                            var _cell_size_physical: int = 1
                            var _cell_height_physical: int = 1
                            
                            # sets up array size holding cells
                            func initialize(size: Vector2i) -> void:
                            	_size = size
                            	_cells.resize(_size.x * _size.y)
                            
                            func _get_array_index(pos: Vector2i) -> int:
                            	assert(pos.x >= 0 and pos.x <= _size.x and pos.y >= 0 and pos.y <= _size.y)
                            	return pos.x + pos.y * _size.y
                            
                            func get_cell(pos: Vector2i) -> Cell:
                            	return _cells[_get_array_index(pos)]
                            
                            func get_cell_pos(pos: Vector2i) -> int:
                            	return _get_array_index(pos)
                            
                            func set_cell(pos: Vector2i,data: Cell) -> void:
                            	print(get_cell_pos(pos), data)
                            	_cells[get_cell_pos(pos)] = data
                            	#var cell = _cells.insert(get_cell(pos),data)
                            	#cell.height = data.height
                            	#cell.walkable = data.walkable
                            
                            # list of all tiles
                            var used_cells: Array[Vector3i]
                            # saves the tile type of each tile in used
                            var cell_item: Array
                            # translates the used_cells coords to Vector2i
                            var cell_coords: Array[Vector2i]
                            
                            func _ready() -> void:
                            	used_cells = get_used_cells()
                            	for cell in used_cells:
                            		cell_item.append(get_cell_item(cell))
                            	for pos in used_cells:
                            		cell_coords.append(Vector2i(pos.x,pos.z))
                            	cell_coords.sort_custom(_sort_descending)
                            	initialize(_get_size(cell_coords[0],cell_coords[cell_coords.size() - 1]))
                            	setup_cell_grid()
                            
                            func setup_cell_grid() -> void:
                            	cell_coords.sort_custom(func(a,b): return a.x + a.y < b.x + b.y)
                            #	var count : int
                            	var grid_count: = Vector2i(0,0)
                            	for cell in cell_coords:
                            		for used in used_cells:
                            			if cell == Vector2i(used.x,used.z):
                            				set_cell(grid_count,Cell.new(0,true))
                            				if grid_count.x < _size.x:
                            					grid_count.x += 1
                            				elif grid_count.y < _size.y:
                            					grid_count.x = 0
                            					grid_count.y += 1
                            				else:
                            					print("what happened?")
                            #				count += 1
                            #				print(count)
                            
                            func _sort_descending(a,b) -> bool:
                            	if a.x + a.y > b.x + b.y:
                            		return true
                            	return false
                            
                            func _get_size(num1: Vector2i, num2: Vector2i) -> Vector2i:
                            	var val_x: int
                            	var val_y: int
                            	if num1.x <= 0:
                            		val_x = abs(num2.x) + num1.x + 1 
                            	elif num2.x < 0:
                            		val_x = abs(num2.x) + num1.x + 1
                            	else:
                            		val_x = num1.x - num2.x + 1
                            	if num1.y <= 0:
                            		val_y = abs(num2.y) + num1.y + 1 
                            	elif num2.y < 0:
                            		val_y = abs(num2.y) + num1.y + 1
                            	else:
                            		val_y = num1.y - num2.y + 1
                            	return Vector2i(val_x,val_y)
                            • xyz replied to this.
                              • Edited

                              xRegnarokx If you plan to use gridmap's cells then best to store your cell data in a dictionary with keys being Vector2i coords of the cell, instead it being an array. Using an array presupposes that all cells in a rectangle of cells defined by _size are populated, however with grid map there's no such constraint.

                              And yeah, you did verbatim copypaste my code. Don't do that. It likely won't work, especially not as an extension of a GridMap node. Write everything from scratch.

                              So if you want to play nice with gridmap in a simple way - maintain a dictionary of cell data objects with key being a Vector2i or Vector3i that represents cells' 2d or 3d coordinate. This will effectively extend gridmap's cells with your custom data.

                              extends GridMap
                              
                              class CellData:
                              	# cell data here
                              
                              var _map_data: Dictionary
                              
                              func _ready():
                              	for cell_coord in get_used_cells():
                              		var cell_data = CellData.new()
                              		match get_cell_item(cell_coord):
                              			0:
                              				# popualte cell_data depending on item	
                              			1:
                              				# popualte cell_data depending on item	
                              			# etc..
                              		_map_data[cell_coord] = cell_data

                                xyz Awesome thanks.

                                I tweaked the code that actually populated the cells from what you wrote, I mainly used the logic of set, get, and it was useful to see how you used the get_index to get a 2d position in a 1d array.

                                But you are right that for what I am trying to do it won't work. Since how I have it set up will only work for rectangular maps.

                                I will start from scratch with what I've learned from your previous suggestion, and your new one suggesting placing the cells in a dictionary with Vector2i/3i as key.

                                I'll have to think through which would be better.

                                Having Vector2i as keys I would have to write a way to have the height of the cell overwrite other lower entries if they are stacked on top otherwise it wouldn't include the top cell.

                                Or to have a dictionary of dictionaries, where the first key is height, then the cells of that height are placed in a dictionary.

                                Or just place an array of cells under keys that have multiple cells at the Vector2i coord.

                                Or calculate all of that and discard any cells that aren't walkable because a cell is on top of it.

                                I'll think through these problems and see what I can do to implement a gridmap based data structure.

                                It may be more streamlined to use the Vector3i coords.

                                • xyz replied to this.
                                  • Edited

                                  xRegnarokx Think about whether you really need stacking of 3d cells. Imo this could better be handled by using only a single floor of gridmap with different cell elevations. You don't need an actual 3d grid.