Unity2D - Randomly instantiated objects need to not spawn close to each other - c#

I'm a beginner to Unity. I'm creating an educational snake game that randomly spawns three potential answers. It's working, however I need to not have them spawn close to each other. I also need not to have any of them spawn near the player/snake. Someone already tried to help me and I asked him for help, but it seems like he's unable to fix the problem and the objects still spawn near each other.
void GeneratePotentialAnswers()
{
allAnswers.Clear();
List<Vector3> AnswerPositions = new List<Vector3>();
AnswerPositions.Add(player.transform.position);
for (int i = 0; i < potentialAnswers.Count; i++)
{
GameObject ans = Instantiate(enemyAnswerPrefab);
Vector3 pos;
int index = 0;
bool tooClose = false;
do
{
float rndWidth = Random.Range(15f, Screen.width - 25f);
float rndHeight = Random.Range(15f, Screen.height - 105f);
pos = Camera.main.ScreenToWorldPoint(new Vector3(rndWidth, rndHeight, 0f));
pos.z = -1f;
foreach (Vector3 p in AnswerPositions)
{
float distance = (pos - p).sqrMagnitude;
if (distance < 3f)
{
tooClose = true;
}
}
index++;
}
while (tooClose == true && index < 500);
Debug.Log(index);
AnswerPositions.Add(pos);
ans.transform.localPosition = pos;
ans.GetComponent<Answer>().potentialAnswer = potentialAnswers[i];
ans.GetComponent<Answer>().addToScore = scorePerCorrectAnswer;
ans.GetComponent<Answer>().SetAnswer();
allAnswers.Add(ans.GetComponent<Answer>());
}
}

Answer
Manually place GameObjects that act as spawnpoints, attach a SpawnPoint script to each of them.
When placing an answer, have the SpawnPoint script of the chosen spawnpoint use a SphereCastAll to check if the nearby area is free, if it is not, another spawnpoint should be selected until a free one is found.
When a free spawnpoint is found, instantiate the answer at its location.
Elaboration
A simple solution would be to have a SpawnPoint script. Which would be a script put on a set of individual spawnpoints you place manually, there could be perhaps 20 of them at various points in the map.
The SpawnPoint script could then use the SphereCastAll method, which gathers a collection of colliders in a zone of a given size, in the shape of a sphere. (Should work in 3d but maybe not 2d, find another cast type than SphereCastAll in that case).
Here is documentation on how to make a SphereCastAll, notice that you can set the radius of the spherecast.
https://docs.unity3d.com/ScriptReference/Physics.SphereCastAll.html
If any objects are struck by the SphereCastAll, they will be stored in the RaycastHit[] it returns. You can search through this, each RaycastHit will have a reference to the collider it hit, along with the GameObject of that collider. Meaning that you have a list of all objects within a zone.
You can then check these objects to see if they are the player or another answer.
Here is documentation for a RaycastHit.
https://docs.unity3d.com/ScriptReference/RaycastHit.html
Note that to gain access to the spawnpoints it would be useful to have a list of them or a manager script of sorts that can select one for you.

Related

OverlapBox returns null almost every time

I've tried to make a script to spawn objects at a random position without them colliding with one another. It doesn't work properly as the OverlapBox returns almost every time null even when it touches a square.
Here is the script:
var quadBoundaries = quad.GetComponent<MeshCollider>().bounds;
var squareRadius = new Vector2(1, 1);
foreach (var square in squaresToSpawn)
{
_isOverlapping = true;
while (_isOverlapping)
{
_spawnPoint = new Vector2(Random.Range(quadBoundaries.min.x + 1.5f, quadBoundaries.max.x - 1.5f),
Random.Range(quadBoundaries.min.y + 1.5f, quadBoundaries.max.y - 1.5f));
_collisionWithSquare = Physics2D.OverlapBox(_spawnPoint, squareRadius,
0, LayerMask.GetMask("Square Layer"));
if (_collisionWithSquare is null)
{
square.transform.position = _spawnPoint;
_isOverlapping = false;
}
}
}
The quadBoundaries are the boundaries of a quad I placed so the squares will randomly spawn in a limited space.
My understanding is that I am generating a random point in the quad boundaries and then I check if on that point a square of scale (1,1) will fit without touching any other thing that has a collider and is on the square layer. if it touches then I generate a new point until the collision is null so I can place the square at the designated position.
But a bunch of things that I don't understand are happening.
First, the squares are touching each other. Second, just a few specific squares are registering a collision but even those are being touched by other squares. Third, when I scale up the square radius (for example 10,10) I get a big split between the squares (shown in the picture bellow).
I must add that all squares have a collider, are all on the square layer and the quad is on a different layer.
Can anybody explain to me what I'm not getting here? Thanks a lot!
Before the answer I'd like to say, that such algorithm of spawning is very dangerous, because you can enteran infinity loop, when there will no be place for new square. Minimum, that you can do to make this code more safe is to add a retries count for finding a place to spawn. But I will leave it on your conscience.
To make this algorithm work, you should understand that all physics in Unity are updated in fixed update. So all operations, that you do with Phisycs or Phisics2D are working with the state of objects, that was performed in the last Pyhsics update. When you change the position of the object, physics core don't capture this changes instantly. As a workaround you can spawn each object in the fixed update separately. Like this:
public class Spawner : MonoBehaviour
{
[SerializeField] private GameObject[] _squaresToSpawn;
[SerializeField] private GameObject _quad;
// Start is called before the first frame update
void Start()
{
StartCoroutine(Spawn());
}
private IEnumerator Spawn()
{
var quadBoundaries = _quad.GetComponent<MeshCollider>().bounds;
var squareRadius = new Vector2(1, 1);
Vector2 spawnPoint;
foreach (var square in _squaresToSpawn)
{
yield return new WaitForFixedUpdate();
var isOverlapping = true;
var retriesCount = 10;
while (isOverlapping && retriesCount > 0)
{
spawnPoint = new Vector2(Random.Range(quadBoundaries.min.x + 1.5f, quadBoundaries.max.x - 1.5f),
Random.Range(quadBoundaries.min.y + 1.5f, quadBoundaries.max.y - 1.5f));
var hit = Physics2D.OverlapBox(spawnPoint, squareRadius,
0, LayerMask.GetMask("Square"));
if (hit is null)
{
square.transform.position = spawnPoint;
isOverlapping = false;
}
else
{
retriesCount--;
if (retriesCount == 0)
{
Debug.LogError("Can't find place to spawn the object!!");
}
}
}
}
}
}
But such code will have effect of continious spawning:
To make objects spawning properly within one frame. You should manualy store all spawned objects bounding boxes inside your code, and manualy check if your new spawn bounding box collide with previously spawned objects.

The most efficient way to track allowed object movement in Unity 2D for puzzle making

Okay so I've rolled this around in my brain, and while I would probably know what code to use if I could figure it out, I just can't decide what the best way to implement this is.
Basically, say I have a vector grid. This grid could have any number of blockaded areas that the item I am pushing around can't go to. The pushing of the object is done by an interact button using the "new" inputsystem package. I could have a dynamic rigidbody and let it be pushed around that way, but I want to use the interact button I have, in part because the first puzzle of this type is used in a tutorial to teach the player the game's commands.
Without knowing where the blockades are going to be, I'm not sure how to tell where the object can move at any given time. My current thought is use colliders, and keep them far enough away that there isn't an actual collision since I don't think I can place them perfectly enough to get collisions at the right times, and throw out raycasts at a short range to detect these colliders. (This gives me more "give" when placing the colliders.)
Does this sound right? Is there a more efficient way to do it? Even if it requires more complex code, I am interested in feedback because I want to learn "good coding" and how to do things in the best, most efficient way possible, rather than spaghetti coding my way through it.
The most efficient way? Forget Colliders. Basic old-school game dev skill: Working with arrays. Create a 2D array of boolean values, or integers set to 0 or 1 representing OPEN or CLOSED.
using UnityEngine;
public class TestObstacleGrid : MonoBehaviour
{
const int GRID_WIDTH = 8; //if you change this, you'll need to match the size of the grid, below.
int[,] grid = {
// -> +x
{0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0},
{0,0,0,1,1,1,1,1},
{0,0,0,1,0,1,1,1},
{0,0,0,1,0,1,1,1},
{0,0,0,0,0,0,0,0},
{1,1,0,0,0,0,0,0},
{1,1,0,0,0,0,0,0}
//|
//V
//+z
};
const int OPEN = 0;
const int CLOSED = 1;
public Vector3 playerPos = Vector3.zero; //give player a start position via inspector.
//You need to drag/drop something like a Cube GameObject into here, via the inspector.
public GameObject playerGameObject;
Vector3 playerOffset = new Vector3(0.5f, 0.5f, 0.5f);
Texture2D tex;
void Start()
{
tex = new Texture2D(8, 8, TextureFormat.ARGB32, false);
tex.filterMode = FilterMode.Point; //makes texture crisp, not blurry
//assumes you are using a plane, not a quad.
this.transform.position = new Vector3(GRID_WIDTH / 2, 0, GRID_WIDTH / 2); //shift by half
this.transform.localScale = new Vector3(-GRID_WIDTH / 10f, 1f, -GRID_WIDTH / 10f); //Unity Planes are 10 units wide.
MeshRenderer renderer = this.GetComponent<MeshRenderer>();
renderer.material.mainTexture = tex;
playerGameObject.transform.position = playerPos;
//Only needs to be called once in Start() or Awake(), if grid doesn't change.
RenderGridToTexture();
}
void Update()
{
Vector3 motion = Vector3.zero;
if (Input.GetKeyDown(KeyCode.W))
motion += Vector3.forward;
if (Input.GetKeyDown(KeyCode.S))
motion += Vector3.back;
if (Input.GetKeyDown(KeyCode.A))
motion += Vector3.left;
if (Input.GetKeyDown(KeyCode.D))
motion += Vector3.right;
Vector3 proposedPos = playerPos + motion;
//prevent player leaving the grid.
proposedPos.x = proposedPos.x < 0 ? 0 : proposedPos.x;
proposedPos.x = proposedPos.x > GRID_WIDTH - 1 ? GRID_WIDTH - 1 : proposedPos.x;
proposedPos.z = proposedPos.z < 0 ? 0 : proposedPos.z;
proposedPos.z = proposedPos.z > GRID_WIDTH - 1 ? GRID_WIDTH - 1 : proposedPos.z;
if (grid[(int)proposedPos.z, (int)proposedPos.x] == CLOSED)
{
proposedPos = playerPos; //reset the proposed to the current position (stay there).
Debug.Log("Invalid attempted move from " + playerPos +
" to " + proposedPos);
}
else //OPEN
playerPos = proposedPos;
playerGameObject.transform.position = playerPos + playerOffset; //offset shifts the cube so it looks right.
//Uncomment this if the grid can change during play.
//RenderGridToTexture();
}
void RenderGridToTexture()
{
for (int z = 0; z < GRID_WIDTH; z++)
for (int x = 0; x < GRID_WIDTH; x++)
tex.SetPixel(x, z,
grid[z, x] == 1 ? Color.red : Color.black );
tex.Apply();
}
}
Create an empty GameObject, and attach to this script along with a MeshRenderer and MeshFilter; for the MeshFilter, give it a Plane mesh. Create a Cube GameObject and drag it onto the script's playerGameObject field in the inspector. The script will do the rest.
Using the keys, your player moves one full square at a time, but you'll want smooth movement in the end. That's fine; this is only a basis for your game. Later on, when you want smooth movement, you can figure it out using Mathf.Lerp() with coroutines to move your player smoothly from one space to another (as well as the object your player is pushing).
You'll need to figure out pushing - the principle is the same as already used here to check for collisions: Look at the grid and see if a non-zero value is there. Then look in front of the object to see if there is an obstacle. Use a different number in the grid, like 2, to represent a pushable object.
That's the best I can do in short form, hope it's enough. Adios and good luck.

Unity, how can I make it so a prefab follows a path?

I'm making a small game in Unity and part of it it's that glass balls spawn every few seconds and they follow a path composed of targets, and the prefab has these targets specified:
Prefab picture
And here is the code:
public class move_to_target : MonoBehaviour
{
public GameObject[] target;
public float speed;
int current = 0;
float radius_target = 1;
// Update is called once per frame
void Update()
{
if (Vector3.Distance(target[current].transform.position, transform.position) < radius_target)
{
current = Random.Range(0, target.Length);
if (current >= target.Length)
{
current = 0;
}
}
transform.position = Vector3.MoveTowards(transform.position, target[current].transform.position, Time.deltaTime * speed);
}
}
So, whenever the ball spawns, it should go for the 1st target, then 2nd and finally the 3rd, but when I load the game, all the balls go to the horizon without stopping.
The problem seems to solve itself when I put the ball/fairy prefab in the scene and load the targets in the scene instead of the prefabs but that isn't the ideal solution.
How can I make it so the balls go to the targets in the scene?
Don't have enough rep to comment but this looks like it should work, what do you see if you Debug.Log(target[current].transform.position)?
If you don't need the target game object references, you could store Vector3[]'s instead.
A couple of things: You're randomizing your target every single update frame, so there is actually no progression from one target to the next in any kind of order. Is that intentional?
Also, you're moving the ball transform only when its distance is less than (<) a certain value. It seems more likely that you should be moving the ball as long as its distance is greater than (>) that value. Are you trying to go from a distance of greater than 1, to a distance of less than / equal to one?
If that's the case, maybe something like (untested):
public List<Transform> target = new List<Transform>();
public float speed;
int current = 0;
float radius_target = 1;
void Update()
{
if (Vector3.Distance(target[current].transform.position, transform.position) > radius_target)
{
transform.position = Vector3.MoveTowards(transform.position, target[current].transform.position, Time.deltaTime * speed);
} else {
current++;
if (current == target.Count)
{
current = 0;
}
}
}
But by this method the ball will seek to return to target[0] after target[2] (or whatever is the last one) and keep going forever in a loop. If you want to stop after reaching the last target you'd want to only execute this whole block if the current target < target.Count without returning to zero as it currently does.
And finally, your radius threshold of 1 could either work or not work, depending on how these targets are set up in the scene. This whole approach only works if they are all more than one unit (or radius_target units) away from each other.

Place object without overlap others

Im having a problem in one of my projects in unity. I basicly have a system where you click an object and then you click somewhere inside an area and the object goes to that position. the problem is that those objects can't overlap. And they can't use physics. to detect colision with each other. I have been working on this for a long time I got something but still not totally working and I hoped someone could help me.
What im basicly doing is getting all objects near the click and then if there are some just calculate the directions between the click and those objects and then add them to the position that seems to work sometimes they don't overlap other times they do and they go to far away and I need them to be near the click.
code:
public Vector3 GetPossiblePosition(List<GameObject> nearbyDiscs, Vector3 position)
{
float initialY = transform.position.y;
Vector3 idealPosition = position;
List<Vector3> directions = new List<Vector3>();
if(nearbyDiscs.Count > 0)
{
foreach (GameObject disc in nearbyDiscs)
{
Vector3 newDirection = position - disc.transform.position;
directions.Add(newDirection);
}
for (int i = 0; i < directions.Count; i++)
{
idealPosition += directions[i] / directions.Count;
List<GameObject> discs = CheckForNearbyDiscs(idealPosition);
if (discs.Count < 1)
break;
}
}
idealPosition.y = initialY;
return idealPosition;
}
behaviour:
You can easily do this using Physics2D.OverlapCircleAll
public static Collider2D[] OverlapCircleAll(Vector2 point, float radius, int layerMask = DefaultRaycastLayers, float minDepth = -Mathf.Infinity, float maxDepth = Mathf.Infinity);
this method returns an array of colliders, you can simply check overlapping if the length of the returning array is greater than one and can handle accordingly.

Error upon spawning multiple boundingBoxes "System.ArgumentOutOfRangeException"

I'm trying to create boundingBoxes (Collision) between an enemy and the bullets that it's struck with. Unfortunately, I'm being presented with the error System.ArgumentOutOfRangeException every time a enemy leaves the screen, or more than 1 bullet is spawned at a time.
Both bounding boxes are created in the Update functions of the bullet & enemy classes as such:
boundingBox = new Rectangle((int)position.X, (int)position.Y, texture.Width, texture.Height);
This is the relevant code being used in my Game1 update function:
for (int i = bullets.Count - 1; i >= 0; i--)
{
bullets[i].Update(gameTime);
ERROR OCCURS HERE -> if (bullets[i].boundingBox.Intersects(enemies[i].boundingBox))
{
score += 1;
bullets.RemoveAt(i);
}
//Bullets being destroyed upon leaving
if (bullets[i].position.Y < 0)
bullets.RemoveAt(i);
}
Any help would be appreciated.
Also, Here is how I implemented the bullet list into the Game1 class if that helps at all?:
List<Bullet> bullets = new List<Bullet>();
public void Shoot (Vector2 pos, Vector2 dir, float speed)
{
Bullet bullet = new Bullet(bulletTexture);
bullet.position = pos;
bullet.direction = dir;
bullet.speed = speed;
if (bullets.Count() < 40) //Maximum amount of bullets
bullets.Add(bullet);
}
If I have to guess, the error comes from the if (bullets[i].boundingBox.Intersects(enemies[i].boundingBox)) statement.
Basically, do you have the same amount of enemies and bullets all the time? If not, then it will fail whenever the numbers are different.
Why is this? Well, you are using two different collections, bullets and enemies, and taking the element at position i.
So if you have 5 bullets but only 1 enemy in the collection, the first iteration of the for will have i = 4, a position that does not exist in the enemies collection (it only has the enemies[0] position).
One way to solve the problem would be:
foreach (enemy in enemies)
foreach (bullet in bullets)
if (bullet.boundingBox.Intersects(enemy.boundingBox))
{
// do your logic here.
}
You go through every enemy, and for each one you check if any bullet is intersecting with it. This is not the best solution performance wise, but I think it's a good start.

Categories

Resources