Wrapping 3D HexMap to camera X position.
Asked Answered
J

13

0

Back in 2019, I had a hex map project that I was initially writing in GDScript, however, GDScript got to be too slow when getting the tiles to follow the camera. I ended up having to rewrite the entire project in C++ because I couldn't figure out how to isolate the code for wrapping the tiles. A few years pass, I'm trying the project again and I'm now to the point where I'd need to get the tiles to wrap around the camera's X position. GDExtension operates differently than GDNative, I would assume. How should I approach this problem? I know we need to get the width of the map by multiplying the width of a HexTile by the number of columns. Once we have the map width, we need to calculate a float of how many mapWidths each tile is from the camera. If the tile is more than 0.5 mapWidths from the camera, the tile should jump to the other side of the map to follow the camera. I'm wanting to write only the code for placing the HexTile in accordance with the camera in C++, instead of the entire game this time.

extends Node3D

var hexTilePrefab: PackedScene = preload("res://Scenes/HexTile.tscn")
var mapSize: Vector2 = Vector2(48, 12)
@export var camera: Node3D = null
var hexTileWidth: float = sqrt(3)
var hexTileArray = [[]]

func _ready():
	generateHexMap()
	
func _process(delta):
	for q in range(-mapSize.x, mapSize.x + 1):
		for r in range(-mapSize.y, mapSize.y + 1):
			checkAndWrapTile(hexTileArray[q][r])

func generateHexMap():
	# Clear the existing array in case this function is called again
	hexTileArray.clear()

	for q in range(-mapSize.x, mapSize.x + 1):
		var columnArray: Array = []  # Create an array for each column
		for r in range(-mapSize.y, mapSize.y + 1):
			var hexTile = hexTilePrefab.instantiate()
			add_child(hexTile)
			hexTile.place_tile(q, r)
			checkAndWrapTile(hexTile)

			# Add the hexTile reference to the array for this column
			columnArray.append(hexTile)
		# Add the column array to the hexTileArray
		hexTileArray.append(columnArray)


func checkAndWrapTile(hexTile):
	# Check the distance between the tile and the camera in world coordinates (X-axis only)
	var tilePosition = hexTile.transform.origin
	var cameraPosition = camera.transform.origin
	var distanceX = abs(tilePosition.x - cameraPosition.x)

	# Calculate how many map widths away the tile is from the camera along the X-axis
	var mapWidthsAwayX = distanceX / (hexTileWidth * mapSize.x)

	# If the tile is more than 0.5 map widths away from the camera along the X-axis, wrap it to the other side
	if mapWidthsAwayX > 0.5:
		# Calculate the new position of the tile to wrap it along the X-axis
		var wrappedPosition = tilePosition
		if tilePosition.x - cameraPosition.x > 0:
			wrappedPosition.x -= hexTileWidth * mapSize.x
		else:
			wrappedPosition.x += hexTileWidth * mapSize.x

		# Set the new position for the tile
		hexTile.transform.origin.x = wrappedPosition.x
extends Node3D

var axial: Vector2
var cube: Vector3
var worldPosition: Vector3

func axialToCube(axial: Vector2) -> Vector3:
	var x = axial.x
	var z = axial.y
	var y = -x - z
	return Vector3(x, y, z)

func cubeToWorld(cube: Vector3) -> Vector3:
	var hexSize = 1.0
	var x = hexSize * sqrt(3) * (cube.x + cube.z / 2)
	var y = 0
	var z = hexSize * 3/2 * cube.z
	return Vector3(x, y, z)

func place_tile(q, r):
	axial = Vector2(q, r)
	cube = axialToCube(axial)
	worldPosition = cubeToWorld(cube)
	transform.origin = worldPosition
Jurisprudence answered 10/10, 2023 at 10:58 Comment(0)
J
0

Updated the script to wrap tiles around the camera, and it works, but the game is starting to lag. I'd like to extract just the code for moving the hex tiles into GDExtension for the sake of speed. How should I go about this?

Jurisprudence answered 10/10, 2023 at 13:39 Comment(0)
H
0

Jurisprudence Why would you want to iterate through all tiles every frame. That's massive overkill. You actually don't need to iterate at all. Put the tiles in a data structure that sorts them by column. You need to wrap only one column at a time. Keep track of which column you wrapped last and you know exactly which is next when the camera pass the threshold.

Herodotus answered 10/10, 2023 at 14:38 Comment(0)
J
0

Herodotus I still need to access each tile individually in order to set its position. If they're put into a data structure (other than an array), the positional data is stored, but unless I tell each tile individually to move to the correct position, their position in the map won't change. I'm likely largely misunderstanding you, or am limited in my knowledge of programming.

Jurisprudence answered 10/10, 2023 at 16:38 Comment(0)
H
0

Jurisprudence Yeah but you don't need to do it every frame for all of them. That's extreme brute force. No wonder you're having lags. You just need one coordinate check per frame - column position vs camera. That's all. And then move the column once in a while. You don't even need to move every tile if you parent each column to a dummy node and just move that node.

Herodotus answered 10/10, 2023 at 16:43 Comment(0)
J
0

Oh, that makes sense. So I create a Node3D for each column that then each tile in that column is a child of, and calculate the X-axis distance of each row, adjusting their position when necessary?

extends Node3D

var hexTilePrefab: PackedScene = preload("res://Scenes/HexTile.tscn")
var mapSize: Vector2 = Vector2(64, 32)
@export var camera: Node3D = null
var hexTileWidth: float = sqrt(3)
var hexMapWidth: float = hexTileWidth * mapSize.x
var columnArray: Array = []

func _ready():
	generateHexMap()
	
func _process(delta):
	# Get the global transform of the camera node
	var cameraTransform = camera.get_global_transform()

	# Iterate through the column nodes in the array
	for columnNode in columnArray:
		# Get the global transform of the column node
		var columnTransform = columnNode.get_global_transform()

		# Calculate the position of the column and camera along the X-axis
		var columnX = columnTransform.origin.x
		var cameraX = cameraTransform.origin.x

		# Calculate the distance between the column and camera along the X-axis
		var distanceX = abs(columnX - cameraX)

		# Calculate how many map widths away the column is from the camera along the X-axis
		var mapWidthsAway = distanceX / hexMapWidth

		# Check if the column is more than 0.5 map widths away from the camera along the X-axis
		if mapWidthsAway > 0.5:
			# Wrap the column to the other side along the X-axis
			var wrapDirection = -1.0 if columnX > cameraX else 1.0
			var wrapDistance = wrapDirection * hexMapWidth
			columnNode.global_translate(Vector3(wrapDistance, 0, 0))

func generateHexMap():
	# Clear the existing array in case this function is called again
	columnArray.clear()

	for q in range(0, mapSize.x):
		var columnNode = Node3D.new()
		for r in range(0, mapSize.y):
			var hexTile = hexTilePrefab.instantiate()
			hexTile.place_tile(q, r)
			columnNode.add_child(hexTile)
		columnArray.append(columnNode)
		add_child(columnNode)

This code is causing the entire map to move at once instead of each column individually...

Jurisprudence answered 10/10, 2023 at 17:47 Comment(0)
H
0

Jurisprudence There's even easier way. Parent each half of map to one dummy node. When the camera passes certain point, simply translate one half into proper place on the other side. This assumes that the map is wide enough. But even if it's narrower you can divide it into as few such chunks as possible and just pop-push them around in a queue.

Herodotus answered 10/10, 2023 at 17:59 Comment(0)
J
0

Herodotus I think what I need to do is generate the map in chunks two tiles thick, then move the chunks in accordance with where the camera is positioned. I still need to be able to access each tile individually for unit placement, tile properties, etc. Even if I do not loop through each tile, I still need to store a reference to each tile. I'm a bit confused as to how to generate the map in chunks. I need the Node3D that parents the hex tiles to be positioned at the center of the tiles it generates. I think the bug I am currently experiencing happens because I created the Node3D's in code and never positioned them, so when I got too far from the nodes they all repositioned themselves at once instead of one at a time as the camera moves.

Jurisprudence answered 10/10, 2023 at 22:1 Comment(0)
H
0

Jurisprudence Simply generate hexes, then make parent nodes and set hexes as children. There's nothing to it. You can have multiple references/lists to same nodes. Have one array that just holds all hexes for normal sequential access and then put node references into whatever acceleration or space partitioning data structure you need for wrapping, so you can access them in more organized/grouped manner. The whole thing can be done in multiple ways, depending on what map sizes and what camera movement you need to have.

Herodotus answered 10/10, 2023 at 22:29 Comment(0)
J
0

Herodotus When I create new Node3D objects to act as the parent, I'm not setting their position, just creating them in code. When I get too far from the Node3D's, they all jump at once. I need to figure out the geometry and proper distance to place each Node3D from the last, that way the tile chunks (Node3D's) register the proper location and move accordingly. With how I'm generating the tiles, they are placed in accordance with their map coordinates, and if I want the chunks to be properly placed, this will not work as their position would now be relative to the Node3D parent, not the root node.

I need to generate the map in chucks and place the chunks after that, so I’ll need to change how the map generator works. Either that, or rewrite performance critical pieces in C++.

Also, I'm using axial coordinates, not offset coordinates, so the grid shape isn't square.

Jurisprudence answered 11/10, 2023 at 14:3 Comment(0)
H
0

Jurisprudence You definitely don't need to go into C++. If you struggle with this in GDScript, messing with C++ will be much harder.

You can simply generate your parent nodes first and place them at the position of a first top-left hex that will be its child. Then generate all hexes, put them into proper global positions and then parent them using Node::reparent(). That function has an optional flag that tells it whether to keep child's global transforms. Setting this flag will keep hexes in place when parenting.

Herodotus answered 11/10, 2023 at 17:10 Comment(0)
J
0

The thing is, the brute force method works, I understand it just fine, and it’s how I wrote it when I last tried this project in 2019. I had to rewrite the entire program in C++, but I had it working well. The computer I was using was orders of magnitude weaker than my current PC. I’m most familiar writing C++ code, and the only reason I struggle with GDScript is because I haven’t used it. I write GDScript using AI because I don’t particularly feel like studying GDScript like I have C++. GDExtension requires more boilerplate code than did GDNative, but I can learn to handle this. The reason I’d rather write this in C++ than figure out how to batch together parts of the map in GDScript is because the way I currently have it, implementing algorithms later will be easier due to my using cubic coordinates. I can’t work on this project currently as I’m not at home, but this is my current directive.

Jurisprudence answered 11/10, 2023 at 18:11 Comment(0)
H
0

Jurisprudence Brute force always works. The question is how efficiently 🙂

This particular problem shouldn't be done in brute force as a much faster solution can be made with a tiny bit of algorithmic thinking.

Exploiting C++ just so you can insist on the simplest brute force solution will eventually bite you in the butt. Because there will appear a problem down the line that even C++ brute force won't be able to outrun. Then what? Why not brush up on your problem solving skills instead and try to devise an optimal algorithm, regardless of the language.

Btw if you're good with C++, GDScript should be trivial to conquer.

Herodotus answered 11/10, 2023 at 18:23 Comment(0)
J
0

Herodotus Perhaps this is something to come back to. I'm really enjoying the fact that we can create new nodes using GDExtension that we can then extend with GDScript later, where as with GDNative, we would create binary "scripts" that would attach to existing nodes, not to be extended with GDScript.

Jurisprudence answered 12/10, 2023 at 15:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.