Top Down 4way Only Movement

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By bipnerds

Hello All,

Main Purpose: I need an entity to move along a given path. The path is known. And it comes in the form of an Array of Vector2. However, it is my main challenge, the movement can only occur horizontally or vertically and not diagonally. In this case the movement flow would be: Move to the next point. If the direction to the next is different, rotate, then move again. So until the end of the path.

One entity that I’ve long wanted to move a path to Spider - KinematicBody2D.

I made a line drawn along the path just allow for viewing.

My script:

extends KinematicBody2D

onready var tilemap = get_node("../TileMap")
onready var tank = get_node("../Tank")
onready var line = get_parent().get_node("./Line2D")

# Movement Variables
var direction = Vector2.UP
var velocity = Vector2.ZERO
var speed = 5000
var threshold = 16

var direction_to_next_point
var world_next_point

func _ready():
	pass

func _process(delta):
	draw_path_line(path_to_target)

func _physics_process(delta):
	# Pathfinding
	path_to_target = tilemap._get_path(tilemap.world_to_map(global_position),
									   tilemap.world_to_map(tank.global_position))
	# I've tested the path and it's returning a correct Vector2 Array.

	# Movement Logic
	if path_to_target.size() > 1:
		world_next_point = tilemap.map_to_world(path_to_target[1])
		if position.distance_to(world_next_point) > 10:
			direction_to_next_point = position.direction_to(world_next_point)
		velocity = direction_to_next_point
	velocity = _to_4way(velocity)
	velocity = move_and_slide(velocity * speed * delta)
	
	# Input Test
	if Input.is_action_just_pressed("test") :
		print('path: ', path_to_target)
		print('direction to next point: ', direction_to_next_point)

# I've created this function to convert diagonal directions on the closer pure horizontal 
# our pure vertical. 
func _to_4way(direction: Vector2):
	var x = direction.x
	var y = direction.y
	if x > 0 and y > 0 :
		if x > y: return Vector2.RIGHT
		if x < y: return Vector2.DOWN
		if x == y: return Vector2.RIGHT
	if x > 0 and y < 0:
		if x > y: return Vector2.RIGHT
		if x < y: return Vector2.UP
		if x == y: return Vector2.RIGHT
	if x < 0 and y > 0 :
		if abs(x) < abs(y) : return Vector2.DOWN
		if abs(x) > abs(y) : return Vector2.LEFT
	if x < 0 and y < 0 :
		if abs(x) < abs(y) : return Vector2.UP
		if abs(x) > abs(y) : return Vector2.LEFT
	return Vector2.ZERO

func draw_path_line(path: Array):
	line.clear_points()
	path.push_front(tilemap.world_to_map(self.global_position))
	for point in path:
		point = tilemap.map_to_world(point)
		point.x += tilemap.cell_size.x/2
		point.y += tilemap.cell_size.y/2
		if !line.points.has(point): line.add_point(point)

But for some reason along the way the velocity has its value changed repeatedly to the equivalent of Vector2.RIGHT and Vector2.DOWN. This creates a strangeness in the spider’s movement and makes it move diagonally. I understand that this is because velocity is being recalculated all the time but I don’t know how to solve it.

Could you help me find a more efficient way to make the movement flow in 4 directions?

You would be better off with astar pathfinding. With astar You will just define location of points on a grid and connect them in square pattern. You will have to learn a bit from docs or tutorials, but it is a simple and ellegant system once You understand it.

Inces | 2022-09-17 18:45

Astar is cool. However, I wouldn’t make any assumptions what pathfinding algorithm they are using here. It may be astar or Godot’s built-in or anything else that fits the project. The depicted movement pattern can be achieved with any pathfinding method.

aXu_AP | 2022-09-17 20:01

Thanks for your answer!

I’m actually using it! The function tilemap._get_path is working just like you said. I’m getting neighbours on four way direction :
# on tilemap _get_path that extends a custom AStar var neighbours = [Vector2.UP, Vector2.RIGHT, Vector2.DOWN, Vector2.LEFT, ]

bipnerds | 2022-09-18 13:25

Glad to be of assistance. Don’t forget to select the answer if it solved the problem! This way someone else might find it easier if they are having similiar problem.

aXu_AP | 2022-09-18 13:55

:bust_in_silhouette: Reply From: aXu_AP

The problem seems to be emerging, when x and y difference is more or less equal. Then spider moves in one frame right and the next frame down, alternating every frame. This would look like it’s going at a 45 degree angle.

You’d want to remember the direction which was last taken, and more likely continue that way. At some point, change direction. You can do this randomly to make movement more natural. Something along these lines (untested):

export(float, 0, 1) var turning_chance = 0.05
var current_direction = "x"
func _to_4way(direction: Vector2):
	var x = direction.x
	var y = direction.y
	
	# At a default 5% chance to change direction randomly
	if randf() < turning_chance:
		current_direction = "x" if current_direction == "y" else "y"
		
	# Make 2 tries to move
    # (without this spider would randomly stop for one frame)
	for i in 2:
		match current_direction:
			"x":
				if x > 0:
					return Vector2.RIGHT
				elif x < 0:
					return Vector2.LEFT
				else:
					# Target on x position, try moving at y axis
					current_direction = "y"
			"y":
				if y > 0:
					return Vector2.DOWN
				elif y < 0:
					return Vector2.UP
				else:
					# Target on x position, try moving at x axis
					current_direction = "x"
	# If we get to end after 2 tries, we must be on target
	return Vector2.ZERO 

The code above could be shortened to a few lines, but I tried to write it as understandable as possible. The turning chance variable affects a lot how the movement feels. Smaller chances for turning yield more determinent movement, bigger chances chaotic ( =1 results in previous behaviour).