Search code examples
game-physicsgame-enginegodotgdscript

Enemies all going to one spot in Godot


I have been trying to make a battle-royale like game where the enemies all target other enemies or players close to them and if there are none they go to random positions. However they are all going to relatively the same spot and i dont know why. Here is my code:

extends KinematicBody2D

onready var player = get_node("../Square_Player")
var Bullet = preload("res://Bullet.tscn")

var time : int = 0

var targets : Array = []
var target : Vector2
var posible_targets : Array = []
var num : int = 0

func _ready():
    for node in get_tree().get_nodes_in_group("targets"):
        if node != self:
            targets.append(node)

func spawn_bullet(target):
    var bullet = Bullet.instance()
    bullet.position = position
    bullet.rotation = rotation
    bullet.target = target
    bullet.p_name = self.name
    get_parent().add_child(bullet)

func _process(delta):

    # using if statement check for error
    if target == null:
        targets.shuffle()
        for t in targets:
            if (t.position - position).length() < 1000:
                posible_targets.append(t)
                num = rand_range(0, posible_targets.size())
                target = posible_targets[num].position
                #target = t.position
            else:
                target = Vector2(rand_range(0, 1000), rand_range(0, 1000))
    else:
        pass

    if target == Vector2(0, 0):
        target = Vector2(rand_range(0, 1000), rand_range(0, 1000))

    else:
        pass

    print(target)
    
    var direction = target - position
    var distance = direction.length()
    direction = direction.normalized()
    position += direction * 200 * delta

    # rotate the enemy to face the player
    var angle = direction.angle_to(Vector2(1, 0))
    rotation = -angle + 89.525

    time += 1

    # if the enemy is close enough to the player, kill the player
    if distance < 1000 and time > 75:
        spawn_bullet(target)
        time = 0

func die():
    # make it invisible
    set_process(false)
    set_visible(false)
    # remove it from the targets group
    if "targets" in get_groups():
        remove_from_group("targets")
    queue_free()

I have tried to change the random numbers that are generated and i have played with different ammounts of enemies to see if the physics has been broken.


Solution

  • First and foremost, remember to call randomize to seed the default random number generator. You can do so in _ready:

    func _ready():
        randomize()
        for node in get_tree().get_nodes_in_group("targets"):
            if node != self:
                targets.append(node)
    

    The random numbers are reproducible, as long as you have the same seed. By calling randomize you make Godot pick a seed based on the current time. Which is what makes it different every time.

    Using randomize will affect shuffle, rand_range and any other function that uses random without a RandomNumberGenerator instance (and if you had a RandomNumberGenerator you would call randomize on it).


    Second, I find this odd:

            targets.shuffle()
            for t in targets:
                if (t.position - position).length() < 1000:
                    posible_targets.append(t)
                    num = rand_range(0, posible_targets.size())
                    target = posible_targets[num].position
                    #target = t.position
                else:
                    target = Vector2(rand_range(0, 1000), rand_range(0, 1000))
    

    So, for each target that satisfies (t.position - position).length() < 1000, you add it to posible_targets and then pick a target from posible_targets at random. So you picking a target at random each time you add a target to posible_targets.

    You could pick once at the end of the loop. Or better yet, you could simply pick the first target that satisfies (t.position - position).length() < 1000, because the targets are already shuffled.

    Like this:

            # if we don't find another target, this will be the value
            target = Vector2(rand_range(0, 1000), rand_range(0, 1000))
    
            targets.shuffle()
            for t in targets:
                if (t.position - position).length() < 1000:
                    target = t.position
                    break                
    

    By the way, I don't see you clean posible_targets, you were always adding, never removing. But, as you can see, you don't need posible_targets anyway.


    I also want to comment on this:

        if "targets" in get_groups():
            remove_from_group("targets")
        queue_free()
    

    You can check if the node is in a group using is_in_group. However, there is little point in removing it since you are releasing the node with queue_free anyway.


    Which brings me to this:

        for node in get_tree().get_nodes_in_group("targets"):
            if node != self:
                targets.append(node)
    

    First of all, know that each time you call get_nodes_in_group, you get a new array. So there is no problem if you modify it. Thus, you can remove self from it:

        targets = get_tree().get_nodes_in_group("targets")
        targets.erase(self)
    

    But since the targets might being released (queue_free). That array would be obsolete.

    Consider:

            # if we don't find another target, this will be the value
            target = Vector2(rand_range(0, 1000), rand_range(0, 1000))
    
            var targets := get_tree().get_nodes_in_group("targets")
            targets.shuffle()
            for t in targets:
                if t != self and (t.position - position).length() < 1000:
                    target = t.position
                    break
    

    Alternatively, add a check to see if you are not trying to access a dead target:

            # if we don't find another target, this will be the value
            target = Vector2(rand_range(0, 1000), rand_range(0, 1000))
    
            targets.shuffle()
            for t in targets:
                if is_instance_valid(t) and (t.position - position).length() < 1000:
                    target = t.position
                    break
    

    You might also be interested in removing said dead targets, but I'll leave that up to you.


    Last but far from least, you have declared target like this:

    var target : Vector2
    

    Which means, this will never be true: target == null.

    This will, however I won't recommend considering Vector(0.0, 0.0) an invalid target (how do you know there isn't a target there?):

    if target == Vector2(0, 0):
            target = Vector2(rand_range(0, 1000), rand_range(0, 1000))
    

    Instead, you might want to pick a target in _ready. Which also suggest to extract the code to pick a target to another function which you can call where you need it.

    This also puts into question when do you want to pick a new target. Perhaps you want to keep a reference to the node you got from get_tree().get_nodes_in_group("targets") and pick a new target when that dies? That, I don't know, it is your game design.