Objects being drawn all in the same location when randomly drawed

So recently i posted a question here for help in generating random objects for my “tower defense” game. I’ve got a lot of help, and actually managed to do it. The problem is the positions where “too random” and it gave me issues with object 1 being drawn above object 2 and things like that. The solution i found was to make and array with 5 different locations for each object, so this way these objects wouldn’t collide with each other.

However, i also needed to make the number of objects random, and i realized an issue. When i get, for example, 2 intances of object 2 drawn on screen, they are getting the same position, and i’m not managing to make them get different positions. I made a function that will select a random number from this “position array”, but how can i do it so it calls a different position everytime it is called? I understand the problem happens because the function is being called once and having the same value forever.

        protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);

        _spriteBatch.Begin(this.camera);
        mapa.draw(_spriteBatch, backGroundFloor);
        for(int i = 0; i < control.randomNumber(); i++) { mapa.drawObjs(_spriteBatch, singlePineTree, rNX1, rNY1); }
		
        player.anim.Draw(_spriteBatch);
        _spriteBatch.End();

        base.Draw(gameTime);
    }

control.randomNumber is the number of times the obj will be drawn on screen. Here’s this simple function:

        public int randomNumber()
    {
        Random rand = new Random();
        return rand.Next(2,4);
    }

I call the function for a random position in initialize or in the constructor ( understand the problem is here, but don’t know where to call it or how to call it to work how i want it):

    protected override void Initialize()
{
    _graphics.PreferredBackBufferWidth = 1920;
    _graphics.PreferredBackBufferHeight = 1080;
    _graphics.ApplyChanges();

    this.camera = new Camera(_graphics.GraphicsDevice);

    rNX1 = control.rNumberPos(15, 50, 900, 600, 700);
    rNY1 = control.rNumberPos(55, 70, 500, 300, 250);

    base.Initialize();
}

and the function:

        public int rNumberPos(int r1, int r2, int r3, int r4, int r5)
    {
        Random rand = new Random();
        int[] rArray = { r1, r2, r3, r4, r5 };
        int rn = rand.Next(0, rArray.Length);
        return rArray[rn];
    }

Each of your mapa.drawObjs needs to have its own rNX and rNY variables.

They are all in the same place because you only have one position (rNX1, rNY1) which is being used by all of them.

You also need some way to check that the random start position for the next drawObjs has already been picked, so that you don’t setup the same X, Y twice.
One way would be to use a List of all the positions (e.g. 0,1,2,3,4) and remove the number from the list when it’s picked. You would pick your random start position index from this list and then get X, Y from the arrays using the number picked.

Also, as a side note, you only need to have one Random class. you can create this at the start and just call Next() when you need a random number.

Hope this helps.

for(int i = 0; i < control.randomNumber(); i++)

FYI, if you have a method call in the condition for a for loop, it will be called on each iteration. If you want to have the same comparison each time, you would need to cache the random value in a variable before starting the for loop:

int randomNumber = control.randomNumber();
for (int i = 0; i < randomNumber; i++)

Also, your randomNumber() method will only ever return either 2 or 3, since the upper bound of random.Next is exclusive. If you wanted it to return 2, 3, or 4, for instance, you would need to call rand.Next(2, 4 + 1); or rand.Next(2, 5);.

You should probably encapsulate trees as their own class. In the class, you could store a Rectangle representing the boundaries of the tree, and the class could have a Draw method. In your map class, you could have a List of tree objects, and a method to randomize the locations of the trees, that you would call once. The method would iterate through the list of trees, generate a random location, and then check if it intersects with the bounds of any of the previous trees, and if it does, then it would generate a new random position until it found one that did not intersect with any of the previous trees. Then, when drawing the map, you would just iterate through the list of trees and draw each one.

Something like this:

class Tree
{
    Texture2D texture;
    Rectangle boundaries;

    public Rectangle Boundaries => boundaries;

    public Point Location
    {
        get => boundaries.Location;
        set => boundaries.Location = value;
    }

    public Tree(Texture2D texture)
    {
        this.texture = texture;
        boundaries.Size = texture.Bounds.Size;
    }

    public void Draw(SpriteBatch spriteBatch) => spriteBatch.Draw(texture, boundaries, Color.White);
}

Then, in your map class…

class Map
{
    Random random = new Random();
    List<Tree> trees = new List<Tree>();

    // Other code

    public void AddTree(Texture2D treeTexture) => trees.Add(new Tree(treeTexture));

    public void RandomizeTreeLocations()
    {
        const int xLimit = 1920;
        const int yLimit = 1080;

        for (int i = 0; i < trees.Count; i++)
        {
            while (true)
            {
                Point randomLocation = new Point(random.Next(0, xLimit), random.Next(0, yLimit));
                trees[i].Location = randomLocation;
                if (!IntersectsWithPreviousTree(i)) // If this tree at its new location doesn't overlap any previous tree...
                    break; // Exit while loop
                // Otherwise, the while loop loops and a new random location is tried.
                // Note that if you have too many or too big of trees such that they can't all fit
                // in the limits specified, this code will get stuck in an infinite loop.
            }
        }

        bool IntersectsWithPreviousTree(int index)
        {
            Rectangle treeBoundaries = trees[index].Boundaries; // Get the tree's boundaries.
            for (int i = index - 1; i >= 0; i--) // Iterate through all trees that preceded the current tree.
            {
                Rectangle previousTreeBoundaries = trees[i].Boundaries;
                if (treeBoundaries.Intersects(previousTreeBoundaries)) // Checks for overlap, or intersection.
                    return true;
            }
            return false; // If this code is reached, then no overlap was found.
        }
    }

    public void Draw(SpriteBatch spriteBatch)
    {
        // Other draw code

        for (int i = 0; i < trees.Count; i++)
            trees[i].Draw(spriteBatch);
    }
}

And finally, in your Game1 class:

// other code

        protected override void Initialize()
        {
            base.Initialize();
            int numberOfTrees = 5;
            for (int i = 0; i < numberOfTrees; i++)
                map.AddTree(treeTexture);
            map.RandomizeTreeLocations();
        }

//other code

Hopefully that helps.

1 Like

Thank you for your response, as it helped me think in a logic i didn’t think before and helped me learn about Point (didn’t knew it) and it deals with collision in a much smarter way than i was thinking.

Just a question: In my tree file, should i load the sprite like i do in my Game1.cs (in a “LoadContent” class)? I ask this because when i run the game with this changes, i get an error in this class that says this:

Althougth i an calling this in my Initialize (in my game1.cs) with the correct sprite name:
Screenshot_250

And am calling the method in my map file, like this:

Screenshot_251

(I changed the name of a few methods for my better understanding)

Sorry, maybe I should have just suggested putting the initialization stuff in LoadContent. The null exception you’re getting means the Texture2D hasn’t been loaded yet.

base.Initialize(); calls LoadContent, so if you need to use loaded content in your Initialize method, you’ll need to put it after base.Initialize();, like this:

protected override void Initialize()
{
    base.Initialize();

    // other code

    int numberObj1 = 5;
    for(int i = 0; i < numberObj1; i++)
    {
        mapa.AddObj(singlePineTree);
    }
    mapa.RandomizeObjLocation();
}

You’ll notice I also moved the mapa.RandomizeObjLocation(); to be outside of the for loop. That’s because, if your RandomizeObjLocation() method is just like the one I made, you only need to call it once, after adding all of the objects. In my original code it may have looked like it was part of the for loop, but it wasn’t, because if you have a for loop with no braces (the { and }), then only the first statement below the for loop is part of the for loop. In other words, the following two for loops are equivalent:

    for(int i = 0; i < numberObj1; i++)
    {
        mapa.AddObj(singlePineTree);
    }
    mapa.RandomizeObjLocation();

    // is the same as

    for(int i = 0; i < numberObj1; i++)
        mapa.AddObj(singlePineTree);
    mapa.RandomizeObjLocation();
1 Like

Thank you again for the explanation! I understand what you mean in everything. Thank you for your patience.

Just a question, is there anything there that could make the game not open? When i run the code exactly like this it doesn’t point me any error, the game just doesn’t open. However if i comment the line:

mapa.RandomizeObjLocation();

The game opens, but without trees (which is the object i’m testing with).

If you think this is a bit confusing i could share the code in github with you or something like that.

I don’t wanna bother, you already helped a lot, just a question if you got the time. Thanks for your attention!

1 Like

Sounds like it’s stuck in an infinite loop. As I noted in the comments in the code, it’s possible for it to get stuck in an infinite loop if the objects are too big and thus don’t fit in the space; it’ll just keep searching forever through random spaces for an empty one and never find one. Could you post your full mapa.RandomizeObjLocation(); method? Also, how big is the tree texture?

Very small, its 99x107:

The full method is this one:

        public void RandomizeObjLocation()
    {
        const int xLimit = 1920;
        const int yLimit = 1080;

        for (int i = 0; i < objects.Count; i++)
        {
            while (true)
            {
                Point randomLocation = new Point(rand.Next(0, xLimit), rand.Next(0, yLimit));
                objects[i].Location = randomLocation;

                if (!CollideWithPreviousTree(i))
                {
                    break;
                }
            }
        }

        bool CollideWithPreviousTree(int index)
        {
            Rectangle limitesObj = objects[index].Limites;

            for (int i = index - 1; i >= 0; i++)
            {
                Rectangle objAnterior = objects[i].Limites;
                if (limitesObj.Intersects(objAnterior)) { return true; }
            }
            return false;
        }
    }

Okay, I found the issue. In your code, the game gets stuck in the for loop inside the method CollideWithPreviousTree because you need to decrement the i variable to move backward through the list of objects. The condition is i >= 0, but since i is only added to, it will always be greater than zero. But it’s weird that it didn’t throw an exception (error), since I would have expected the i in your code to quickly exceed the number of objects…

Instead of i++, it should be i--, like this:

        bool CollideWithPreviousTree(int index)
        {
            Rectangle limitesObj = objects[index].Limites;

            for (int i = index - 1; i >= 0; i--)
            {
                Rectangle objAnterior = objects[i].Limites;
                if (limitesObj.Intersects(objAnterior)) { return true; }
            }
            return false;
        }

Let me know if that fixes it. I can’t help but think there might be something else going on as well since an exception wasn’t thrown.

1 Like

The game opens like this! But no tree is displayed. I went to double check but i seems the sprite and everything is fine. I made a test with an old method, lilke this:

        public void drawObjs(SpriteBatch spriteBatch, Texture2D texture, int r, int nX, int nY)
    {
        for (int i = 0; i < r; i++)
        {
            spriteBatch.Draw(texture, new Vector2(objLocationsX[i] + nX, objLocationsY[i] + nY), Color.White);
        }
    }

        public Mapa()
    {
        Random random = new Random();
        for (int i = 0; i < objLocationsX.Length; i++)
        {
            objLocationsX[i] = random.Next(50, 300);
            objLocationsY[i] = random.Next(50, 300);
        }

    }

And that’s how i drawn them (in Game1.cs, on the draw method):

mapa.drawObjs(_spriteBatch, singlePineTree,5, 300, 100);

This way i can get the objs to being drawn in a “kinda random way”, but your way is more organized and better to deal with the collisions (idk how to deal with collisions this way). Do you think maybe something in the Draw method is avoiding the trees from being drawn? (I put my code just for example, i really want to make it work the other way as it’s seems more practical to work with).

Btw, this is my initialize method:

        protected override void Initialize()
    {
        _graphics.PreferredBackBufferWidth = 1920;
        _graphics.PreferredBackBufferHeight = 1080;
        _graphics.ApplyChanges();

        this.camera = new Camera(_graphics.GraphicsDevice);

        base.Initialize();

        int numberObj1 = 5;
        for (int i = 0; i < numberObj1; i++)
            mapa.AddObj(singlePineTree);
        
        mapa.RandomizeObjLocation();
    }

This worked great, it was my fault for passing the wrong value in the draw method and not noticing it.

Once again thanks to @HopefulToad for the help as it gave me a lot of new information about monogame and helped me a lot with ideas (with Point and Intersects method)

Once again thanks for the attention!

No problem! So you got it working, then?

1 Like

Yes, i did! The way you taught is great and worked perfectly, actually. I was able to now move to the next step of my game, which is collisions, as soon as i finish working today in a few hours gonna try do make the collisions work. Hopefully there’s a lot of content on youtube about it! Thank you very much once again!

1 Like