Hello, I'm having serious troubles getting actors not to clip into each other. This is for a grid based game, so we have to detect collision before committing to move a certain distance. Tile size is 16x16. All collision shapes and shapecasts are 15x15 squares (because shapecasts recognize adjacent collision shapes).
Here's the gist: AI initiates a movement. We force a shapecast update, then do a shapecast check. If the shapecast check is successful (no collision), it initiates movement and occupies the target position with a second collision shape (which is a child node of the actor, but is held in place using inverse movement). All of this happens within the same frame. Giving 2 actors different process priorities should mean their individual _process functions run one after the other, which means that they should never clip into each other, as the first actor occupies the target position and the second actor would recognize that there is a collision box there and not initiate movement. However, if I let 2 actors that are one step size apart run into each other on the same frame (happens with step size at both full tile size and fractions), they still clip into each other. Does process priority not work like that?
Best solution I have for this problem would be to do a shapecast check and occupy the target position, wait for the next physics frame, then do another shapecast check, and only if the second shapecast is also successful do I let the actor move, otherwise remove I the occupying collision shape. But this is getting ridiculously complex. There must be something simple that I'm not getting here. Any help is welcome.
Below is the code for an NPC. This one always moves left, then right.
extends Area2D
#get tile dimensions from autoload
const tilesize = g.tilesize
const stepsize = g.stepsize
#get movement speed (/ stepsize) from autoload
var walkspeed = g.walkspeed_fast
#variables for walk movement logic
var initpos = Vector2.ZERO
var dir = Vector2.ZERO
var moving = false
var walkprog = 0.0
#variables for local timer
var timercount = 0.0
var timerstate = true
#delay in seconds between ai steps
var ai_delay = 0.0
#list of ai steps to cycle through
var ai_steps = [Vector2.LEFT,Vector2.RIGHT]
#current step in ai cycle
var ai_step = 0
#child nodes used by this script
@onready var collisionshape = $ShapeCast2D
@onready var collisiontarget = $"CollisionShape2D-Target"
@onready var animatedsprite = $AnimatedSprite2D
# Called when the node enters the scene tree for the first time.
func _ready():
#snap actor position to grid (fail-safe, should not be necessary)
position = position.snapped(Vector2.ONE*stepsize)
#initial position for movement calculation
initpos = position
#animate
animatedsprite.speed_scale = 0.3
animatedsprite.play("front")
# everything runs in the same physics frame if possible, no unnecessary delays
func _process(delta):
timer(delta)
ai()
move(delta)
func timer(delta):
#increment timer by delta (1 whole number per second) if active
if timerstate == true:
timercount += delta
func ai():
#if not currently moving and if timer has passed delay value (in seconds)
if moving == false and timerstate == true and timercount >= ai_delay:
#set walk direction to current ai step
dir = ai_steps[ai_step]
#turn sprite
if dir.y == 1:
animatedsprite.play("front")
elif dir.y == -1:
animatedsprite.play("back")
elif dir.x == 1:
animatedsprite.play("right")
elif dir.x == -1:
animatedsprite.play("left")
#shapecast to target position
collisionshape.target_position = dir * stepsize
collisionshape.force_shapecast_update()
#if target position is not occupied
if !collisionshape.is_colliding():
#enable second collision box (to occupy target position)
collisiontarget.disabled = false
#set initial position for calculating the walk movement
initpos = position.snapped(Vector2.ONE*stepsize)
#disable and reset timer
timerstate = false
timercount = 0
#increment ai step
ai_step = c.bndInc(ai_step,len(ai_steps)-1)
#enable move function
moving = true
#speed up animation
animatedsprite.speed_scale = 1
func move(delta):
if moving == true:
#walk progress += 1 delta increment * walk speed
walkprog += walkspeed*delta
var a = stepsize*dir
var b = a*walkprog
var c = tilesize/2
#move actor by (1 delta increment * walk speed)
position = initpos + b
#second collision box occupies target position, must stay in place
#current position to occupy is (walk target - walk progress + center offset)
collisiontarget.position = a - b + Vector2(c,c)
#if actor arrived at target position
if walkprog >= 1.0:
#player is 1 full stepsize away from beginning
position = initpos + (stepsize*dir)
#reset variables to wait for new input
walkprog = 0
moving = false
timerstate = true
#disable second collision box until it is needed again
collisiontarget.disabled = true
#slow down animation
animatedsprite.speed_scale = 0.3
Autoload "g":
extends Node
const tilesize:float = 16.0
const stepsize:float = 8.0
const walkspeed_fast = 24.0/stepsize
Autoload "c":
extends Node
#"bounded" increment
func bndInc(value:int,limit:int,increment:int=1,wraparound:int=0):
value +=increment
if value > limit:
value=wraparound
return(value)