So, I am trying to get a grid based movement in 3D, I have it working, except for my ray collision which works, but it doesn't allow for the current movement to finish, and then stop it.

Example:
I want to move 1 meter increments, if there is no new direction input I want to continue if the button is still pressed, however, if a new button is pressed I want to go that direction if there isn't a collision. If both the original button and the new direction that has a collision are pressed I want it to continue in the original buttons direction.

Also, if you move and a collision is detected I want it to finish out the movement increment so that it is at a 1 meter increment, rather than instantly stopping. Currently the way I have my code it stops instantly.

extends CharacterBody3D


const SPEED = 2.5

var move_increment : float = .04
var moving : bool
var move_timer : float
var move_queue : Array
var current_move := Vector3(0,0,0)
var direction : Vector3

@onready var ray := $RayCast3D

func _input(event: InputEvent) -> void:
	var move_action := {
		"north":Vector3(-1,0,0),
		"south":Vector3(1,0,0),
		"east":Vector3(0,0,-1),
		"west":Vector3(0,0,1),
		}
	for action in move_action:
		if event.is_action_pressed(action): 
			# Checks if this is a duplicate; Moves input to front of queue
			if not move_queue.has(move_action[action]): 
				move_queue.push_front(move_action[action]) 
				if current_move == Vector3(0,0,0):
					current_move = move_action[action]
				break
			# Checks if event is input release; removes movement from queue
		elif event.is_action_released(action): 
			move_queue.erase(move_action[action])
			break

func _physics_process(_delta: float) -> void:
	if not current_move == Vector3(0,0,0):
		moving = true

	if moving:
		if not move_queue.is_empty():
			direction = (transform.basis * Vector3(current_move.x, 0, current_move.z)).normalized()
		ray.set_target_position(Vector3(direction.x,-direction.z,0))
		ray.force_shapecast_update()
		if ray.is_colliding():
			moving = false
			move_timer = 0
			move_queue.erase(current_move)
			if move_queue.is_empty:
				current_move = Vector3(0,0,0)
			else:
				current_move = move_queue[0]
		if not move_timer < 1:
			moving = false
			move_timer = 0
			if move_queue.is_empty():
				current_move = Vector3(0,0,0)
			else:
				current_move = move_queue[0]
			position.snapped(Vector3(0.5,0.5,0.5))
			return
		else:
			move_timer += move_increment

		if direction:
			velocity.x = direction.x * SPEED
			velocity.z = direction.z * SPEED

		move_and_slide()
  • xRegnarokx replied to this.
  • xRegnarokx Yes. Instead of an array you can use a dictionary with Vector2i keys for a faster lookup. If a key is in the dictionary it means that location is occupied.

    If you need pathfinding, use A*. Otherwise don't.

    xRegnarokx So, for those interested in this question, I do have an update. I figured out my movement, with only two problems left to rectify, I don't know how to get it to remember and switch to the newly pressed buttons once it is no longer colliding. Also, I just now discovered that if I am moving directly at an object it detects collision and stops at the range of my ShapeCast, so either I need to finetune that distance, or figure out a way to finish out the movement if the collision didn't happen before the start.

    Example of button press:
    if I press up, and am moving up, and I press left while still holding up, however the left direction has a wall, I want it to start moving left the moment there is no longer a collision in that direction, yet in the meanwhile to keep moving forward. Like in some games that allow you to "queue" up movement for turning around corners before hand, rather than having to remove input from one key and pressing another.

    extends CharacterBody3D
    
    
    const SPEED = 2.5
    
    var moving : bool
    var move_queue : Array
    var current_move := Vector3.ZERO
    var start_pos := Vector3.ZERO
    var direction : Vector3
    
    @onready var ray := $RayCast3D
    
    func _input(event: InputEvent) -> void:
    	var move_action := {
    		"north":Vector3(-1,0,0),
    		"south":Vector3(1,0,0),
    		"east":Vector3(0,0,-1),
    		"west":Vector3(0,0,1),
    		}
    	for action in move_action:
    		if event.is_action_pressed(action): 
    			# Checks if this is a duplicate; Moves input to front of queue
    			if not move_queue.has(move_action[action]): 
    				move_queue.push_front(move_action[action])
    				if not current_move:
    					current_move = move_action[action]
    				break
    			# Checks if event is input release; removes movement from queue
    		elif event.is_action_released(action): 
    			move_queue.erase(move_action[action])
    			break
    
    func _physics_process(_delta: float) -> void:
    	if current_move:
    		moving = true
    
    	if moving:
    		direction = (transform.basis * Vector3(current_move.x, 0, current_move.z)).normalized()
    		velocity.x = direction.x * SPEED
    		velocity.z = direction.z * SPEED
    		ray.set_target_position(Vector3(direction.x,-direction.z,0))
    		ray.force_shapecast_update()
    		if not start_pos:
    			start_pos = position
    
    		if ray.is_colliding():
    			var move_again = move_queue.pop_at(move_queue.find(current_move))
    			if not move_queue.size() < 2:
    				current_move = move_queue.front()
    				direction = (transform.basis * Vector3(current_move.x, 0, current_move.z)).normalized()
    				velocity.x = direction.x * SPEED
    				velocity.z = direction.z * SPEED
    				print(current_move,move_queue)
    			else:
    				current_move = Vector3.ZERO
    		
    		if current_move:
    			if start_pos.distance_to((position + current_move)) <= 2:
    				move_and_slide()
    			else:
    				position = position.snapped(Vector3(1,1,1))
    				start_pos = Vector3.ZERO
    				if not move_queue.is_empty():
    					current_move = move_queue[0]
    				else:
    					current_move = Vector3.ZERO

    So, I tried a few things, for now I gave up on the delayed button response movement after collision going away. I fixed the other problem by just fine tuning the ray size.

    However, I have a slight stutter I've noticed, I am pretty confident it is caused by snapping the player to meter increments when they are slightly off. Specifically (in my new edited code, not shown here) in the else: under ray.is_colliding. Because it interrupts movement the position can be as much as .15 meters off, which makes for a more visible snap effect, especially when you are changing direction and it snaps. This also is augmented by the fact the camera is attached to the player.

    So, I have it working better, there are still some bugs, every now and then it gets stuck on a wall, trying to debug it to figure out why, I have the ShapeCast just slightly smaller than the character size so it checks to area that the character will soon be occupying. I'll keep working at the bugs, here is the code that I have thus far.

    extends CharacterBody3D
    
    
    const SPEED = 5
    
    # Used as movement perameters
    var moving : bool
    var move_queue : Array
    var current_move := Vector3.ZERO
    var start_pos := Vector3.ZERO
    var direction : Vector3
    
    # Used for detecting collisions for movement
    @onready var ray := $RayCast3D
    
    func _ready() -> void:
    	apply_floor_snap()
    
    # Controls populating move_queue
    func _input(event: InputEvent) -> void:
    	var move_action := {
    		"north":Vector3(-1,0,0),
    		"south":Vector3(1,0,0),
    		"east":Vector3(0,0,-1),
    		"west":Vector3(0,0,1),
    		}
    	for action in move_action:
    		if event.is_action_pressed(action): 
    			# Checks if this is a duplicate; Moves input to front of queue
    			if not move_queue.has(move_action[action]): 
    				move_queue.push_front(move_action[action])
    				if not current_move:
    					current_move = move_action[action]
    				break
    			# Checks if event is input release; removes movement from queue
    		elif event.is_action_released(action): 
    			move_queue.erase(move_action[action])
    			break
    
    # Controls gridbased movement at 1 meter increments
    func _physics_process(delta: float) -> void:
    	# Gravity
    	if not is_on_floor():
    		velocity += get_gravity() * delta
    		move_and_slide()
    	# Sets direction, and shapecast direction
    	direction = (transform.basis * Vector3(current_move.x, 0, current_move.z)).normalized()
    	ray.set_target_position(Vector3(direction.x,0,direction.z))
    	ray.force_shapecast_update()
    	# Checks to see if there is a collision in direction
    	if ray.is_colliding() and not moving:
    		# Checks to see if move_queue has an entry; Sets new direction; Removes current move from queue
    		if move_queue.size() >= 2:
    			move_queue.erase(current_move)
    			current_move = move_queue.front()
    			direction = (transform.basis * Vector3(current_move.x, 0, current_move.z)).normalized()
    			velocity.x = direction.x * SPEED
    			velocity.z = direction.z * SPEED
    			moving = false
    		# If no new direction resets direction to ZERO
    		else:
    			move_queue.erase(current_move)
    			current_move = Vector3.ZERO
    			moving = false
    	elif current_move:
    		velocity.x = direction.x * SPEED
    		velocity.z = direction.z * SPEED
    		moving = true
    	# Checks to confirm that current_move isn't ZERO
    	else:
    		moving = false
    
    	# Guards movement from repeating
    	if moving:
    		# Sets starting position of movement for calculating end of movement
    		if not start_pos:
    			start_pos = position
    		# Checks that move distance hasn't been completed
    		if start_pos.distance_to((position + current_move)) <= 2:
    			move_and_slide()
    		# Snaps position to meter increment, and resets start_pos, and sets new current_move
    		else:
    			start_pos = Vector3.ZERO
    			if not move_queue.is_empty():
    				current_move = move_queue[0]
    			else:
    				current_move = Vector3.ZERO
    			moving = false
    		position.snapped(Vector3(1,1,1)) 

    Edit: I realized I did a stupid and didn't actually apply the snapping to the position, I needed to go position = position.snapped(Vector3(1,1,1) but I didn't haha. So, I added some improvements and so far the bugs haven't shown up in my testing, and it also allows for smoother scaling with speed.

    	if moving:
    		# Sets starting position of movement for calculating end of movement
    		if not start_pos:
    			start_pos = position
    		# Checks that move distance hasn't been completed
    		if start_pos.distance_to((position + current_move)) <= 2:
    			move_and_slide()
    			position = position.snapped(Vector3(SPEED * 0.01,SPEED * 0.01,SPEED * 0.01)) 
    		# Snaps position to meter increment, and resets start_pos, and sets new current_move
    		else:
    			position = position.snapped(Vector3(1,1,1)) 
    			start_pos = Vector3.ZERO
    			if not move_queue.is_empty():
    				current_move = move_queue[0]
    			else:
    				current_move = Vector3.ZERO
    			moving = false
    • xyz replied to this.

      xRegnarokx Why would you need collisions, let alone shapecasts, for grid movement? Simply maintain an occupancy data structure instead. Much less trouble.

        xyz So for this would you suggest I just have an array of occupied locations that I search before moving?

        Array [occupied location]
        If not array.has[position + dir]
        Array.append[position + dir]
        Array.erase[position]
        Move

        Something like this?

        Also, would you suggest having an astar3d to handle terrain, so that only non terrain objects need to be in the array. This would shrink the size of the array when checking for occupancy.

        • xyz replied to this.

          xRegnarokx Yes. Instead of an array you can use a dictionary with Vector2i keys for a faster lookup. If a key is in the dictionary it means that location is occupied.

          If you need pathfinding, use A*. Otherwise don't.

            xyz Sounds good, ahh that dictionary suggestion is good, it would also allow me to store what is there if I wanted to. A* could be useful for ai pathfinding in the future.