Some Handy Procedural Methods (Placing objects on the ground in circle/grid patterns or individually)

posted in Septopus for project Unsettled World
Published January 18, 2019
Advertisement

It's been a while since I posted any code..  Here are some handy methods that I've been using frequently in my SlingBots game.  Keep in mind, some examples use local variables where it's better to reuse variables with more scope for memory/GC reasons in practical application. ;)   If you see something that I could really improve, please let me know.  Many of these are "setup" routines so I've not bothered with optimization really at all since they run once, or very infrequently.

All of these are designed to work with the standard Unity orientation, with normal terrain objects or terrain meshes.

RayCast Position of Ground at any point on terrain:


    Vector3 GroundPosAt(Vector3 position)
    {
        Vector3 result = position;
        //some altitude above the highest possible ground position.
      	float checkAltitude = 2000.0f;
        Ray rr = new Ray(new Vector3(position.x, checkAltitude, position.z), -Vector3.up);
        //I use a fixed allocation ray cast as a general practice to keep GC down.
        //It will need to be big enough to contain all the possible hits it could catch finding your ground object.. 
        //YMMV with these *NonAlloc methods, but they work wonderfully for me.
        RaycastHit[] hits = new RaycastHit[20];
        if (Physics.RaycastNonAlloc(rr, hits) > 0)
        {
            foreach(RaycastHit hit in hits)
            {
                //make sure this entry isn't null
                if (hit.collider != null)
                {
					//check for collision with object tagged ground
                    if (hit.collider.CompareTag("ground"))
                    {
                        result = hit.point;
                    }
                }
            }
        }

        return result;
    }

Get A Vector3 position on a Circle at a specific angle and radius:


    Vector3 CirclePos(Vector3 center, float radius, float angle)
    {
        Vector3 pos;
        pos.x = center.x + radius * Mathf.Sin(angle * Mathf.Deg2Rad);
        pos.y = center.y;
        pos.z = center.z + radius * Mathf.Cos(angle * Mathf.Deg2Rad);
        return pos;
    }

Object Placement using fixed arrays for object tracking and management:

This example shows one of the ways I programmatically instantiate turrets into SlingBot Boarding.


    public GameObject worldTurretPrefab;    
    GameObject[] worldTurrets = new GameObject[100];
    SnowBallTurret[] _worldTurrets = new SnowBallTurret[100];
	

    void PlaceWorldTurret(GameObject parentobject, Vector3 position, int firefrequency, float firepower, float sightdistance)
    {
        for (int i = 0; i < worldTurrets.Length; i++)
        {
            if (worldTurrets[i] == null)
            {
                if (parentobject == null)
                {
                    worldTurrets[i] = Instantiate(worldTurretPrefab);
                }
                else
                {
                    worldTurrets[i] = Instantiate(worldTurretPrefab, parentobject.transform, false); 
                }
                _worldTurrets[i] = worldTurrets[i].GetComponentInChildren<SnowBallTurret>();
                _worldTurrets[i].playerAvatar = GameController.GControl.Player;
                _worldTurrets[i].fireFrequency = firefrequency;
                _worldTurrets[i].id = (i + 1).ToString();
                _worldTurrets[i].turretProjectileVelocity = firepower;
                _worldTurrets[i].turretSightingDistance = sightdistance;
                worldTurrets[i].transform.localPosition = position;
                break;
            }
        }
    }

I use an array for both the object and a reference to the script it holds to save time on future getcomponent lookups.  This will come in handy when I want to upgrade the settings on all of the already instantiated objects due to a player increase in level/etc. 

I use a fixed array so I can predict(reasonably) what the upper level of memory usage will be(after testing).  I iterate through the existing collection and only add a new object if there is an empty slot in the array.  This allows me to ensure there will be ZERO runtime exceptions related to object instantiation.  It is better for my game to end up being a little easier than it should have been than it would be for an exception to be thrown right at an exciting moment.  Note: the above method could be easily modified to return a bool on success/failure if you wanted to adjust the object arrays and try again on failure. ;)

Putting this all together, here's instantiating Turrets in a circle around a specific point(on uneven terrain):


    void PlaceTurretsCircle(Vector3 position, int turretcount, float turretradius)
    {
        //place turrets in circle
        for (int i = 0; i < turretcount; i++)
        {
            //Adjust settings based on players level
            float levelModifier = 1.0f;
            if (currentGameLevel > 1)
            {
                //add 20% to modifier for each level
                levelModifier += (0.2f * currentGameLevel);
            }
            //Calculate angle for object around circle
            float angl = (360 / turretcount) * i;
            PlaceWorldTurret(null,
                             GroundPosAt(CirclePos(position, turretradius, (360 / turretcount) * i)),
                             (int)(2000.0f / levelModifier),
                             50.0f * levelModifier,
                             500.0f);
        }
    }

and a bonus, Here's placing turrets on a Grid:

This one is presently written to require a parent object as it places the turrets in relative position to that object.


void PlaceTurretsGrid(GameObject parentobject, float xstart, float zstart, float xrowdist, float zrowdist, float yoffset, int xrowcount, int count, int firefrequency, float firepower, float sightdistance)
    {
        float xoffset = xstart;
        float zoffset = zstart;
        if (count > 100) count = 100;
        int xmaxcount = xrowcount - 1;
        int xcount = 0;
        for (int i = 0; i < count; i++)
        {
            //Without ground position checking
            PlaceWorldTurret(parentObject, new Vector3(xoffset, yoffset, zoffset), turretFireFrequency, turretFirePower, turretSightDistance);
            //With ground position checking(untested)
            //PlaceWorldTurret(parentObject, GroundPosAt(parentObject.position + new Vector3(xoffset, yoffset, zoffset)), turretFireFrequency, turretFirePower, turretSightDistance);
            xcount++;
            xoffset += xrowdist;
            if (xcount > xmaxcount)
            {
                xcount = 0;
                xoffset = xstart;
                zoffset += zrowdist;
            }
        }
    }

Not a lot of rocket science going on here, but it could be a time-saver or a mental-block fixer for somebody I'm sure. ;)

Check out the game if you get a chance:

https://www.kongregate.com/games/WilliamOlyOlson/slingbot-boarding/

Happy coding out there!! :D

3 likes 0 comments

Comments

Rutin

Thanks for sharing. :) 

January 18, 2019 05:58 AM
Septopus
12 minutes ago, Rutin said:

Thanks for sharing. :) 

Happy to!  I have a hard time remembering to post stuff like this, if there's anything in the game you(or anybody) would like me to elaborate on in code form feel free to let me know.  I often forget how long it takes me to figure things out and begin looking at them as simple bits of code that don't really need sharing.

January 18, 2019 06:14 AM
ferrous

You could drastically speed up your raycast/ground check by using a layermask.  If you only checked for objects on the ground layer, there would be no need to compare tags.

In fact, at that point, depending on your game, you might not even need the raycastnonalloc, if for example your terrain was a single Terrain.

January 18, 2019 09:06 PM
Septopus
18 minutes ago, ferrous said:

You could drastically speed up your raycast/ground check by using a layermask.  If you only checked for objects on the ground layer, there would be no need to compare tags.

In fact, at that point, depending on your game, you might not even need the raycastnonalloc, if for example your terrain was a single Terrain.

Good point, I always overlook layermasks for raycast uses...  Probably because they confused the heck out of me for a while there.  That would certainly help in dense object scenarios.  But regardless of how many results there are, NonAlloc still results in less GC due to internal garbage generated by the normal raycast methods.  At least in my testing.

Thanks for reminding me about layermasks! :D

January 18, 2019 09:27 PM
ferrous

I mean that you no longer need to check for multiple hits, therefore don't need to use the Physics.Raycast that returns multiple hits, which should theoretically invoke much less garbage collection than yours,since no array is required.  

January 18, 2019 09:34 PM
Septopus
21 minutes ago, ferrous said:

I mean that you no longer need to check for multiple hits, therefore don't need to use the Physics.Raycast that returns multiple hits, which should theoretically invoke much less garbage collection than yours,since no array is required.  

Like I stated at the top, some of these are unoptimized examples using local variables.  I reuse non-local arrays to keep from generating excess garbage in production code.  I started out using normal raycasts(w/o layermasks of course), it's also important to mention that there's really nothing to mask out in most of my use cases.  As the ground object is the only thing that can be collided with.  Profiling got me to my present methodologies. ;) I shall now venture to test if layermasking reduces the GC as well.  As it stands I don't have any GC of measure. :D

 

I'm also curious if a single result raycast would guarantee the closest hit as my more resource intensive casts are not straight up and down and often return multiple ground hits that are never in a predictable order...  More tests, perhaps the topic of my next blog. ;)

 

January 18, 2019 09:56 PM
ferrous

If your scene has 100 objects, your original code will raycast against those 100 objects, looking for hits, copy the results of up to twenty objects that hit the ray, and then you check if those 20 objects are ground objects.   By using layermasking, we first filter out the 100 objects for ground objects.  (Of which there might be only one!)  Then we do a ray test on those objects.  So even if there is no GC difference, there should be a order of magnitude difference, especially as object count and complexity goes up.  That and we eliminate the tag check as well.

I don't recall if the single hit method returns the closest or not, it would be pretty garbage if it didn't, but I wouldn't be surprised if it didn't, or if it generated more GC that the non-alloc.  Though I wonder how much it of a difference there is if compiling to their il2cpp code.

 

January 18, 2019 10:12 PM
Septopus

Yeah, you've got me curious on a number of things too.  I noticed significant improvements when switching to this method, and not just in GC numbers.  So now I'm going to have to compare it to single result and/or layermasked methods.

And actually the more I think about it, I have currently over 100 ground objects(tiles), and maybe 2 others that "might" get in the way sometimes..  So it's actually backwards, I think layermasking would help(Me) most of I was looking for something other than the ground..   Hahaha!

January 18, 2019 10:20 PM
ferrous

Haha, now you have me curious what your scene is like, as you shouldn't be getting amazing improvements if you're mostly 'ground' objects.  Unless CompareTag is slow.  (Which I suspect it might be)  I'm also not sure what kind of spatial partitioning that unity does.  So it might be trying to check against more objects than you think.

Anyway, Physics layers and Layer masking are great ways to improve performance.  So definitely make sure you understand them.  

January 18, 2019 11:25 PM
Septopus
1 hour ago, ferrous said:

Haha, now you have me curious what your scene is like

Game link is above. ;)

Quote

as you shouldn't be getting amazing improvements if you're mostly 'ground' objects.

To be honest my memory for how much something improved isn't always 100% accurate.  I do remember it improving things to some noticeable degree, and it may have just been the GC. ;) 

Ultimately though, so long as I'm getting close to 60fps on a web build, I call it optimized and move on to the next dumpster fire. :D

Premature optimization and all that.  And I got a lot of dumpster fires...

Quote

  Unless CompareTag is slow.  (Which I suspect it might be)

It's better than a standard string comparison, so.. aside from switching to a layermask method(which I'm not against), only way to go.

Quote

  I'm also not sure what kind of spatial partitioning that unity does.  So it might be trying to check against more objects than you think.

I'm currently set to 16 subdivisions on a 10000x10000x10000 world bounds.  (but I have no idea how those work either)  The terrain is approximately the same size(x & z).

Quote

Anyway, Physics layers and Layer masking are great ways to improve performance.  So definitely make sure you understand them.  

For sure, I use layers for cameras and segregating colliders that shouldn't interact, just not raycasts, currently, in this game.. ;)

January 19, 2019 01:08 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement