I am using Unity's Sprite Shapes with Edge Colliders as terrain for my orthographic 2d sidescrolling game, with multiple 'tiles' (i.e. a gameobject with the Sprite Shape and Edge Collider classes).
The intention is, when the camera is one screen's width away from the end of the current 'tile', to instantiate a random terrain from a selection terrains, then place the first vector of the next collider at the same position of the last vector of the previous collider. That way, they connect.
However, when the camera gets to the specified point, the script continues to load terrains infinitely and in the wrong place.
Please let me know if further detail is required.
using System.Collections.Generic;
using UnityEngine;
public class TerrainManager : MonoBehaviour
{
public Transform[] terrains;
public Transform terrainVisualOutput;
private Transform m_lastTerrain { get { return m_displayedTerrains[m_displayedTerrains.Count - 1]; } }
private List<Transform> m_displayedTerrains = new();
static System.Random m_rnd = new();
void FixedUpdate()
{
// If within frame width's distance of last point, add terrain and remove previous terrain
if (Camera.main.transform.position.x + GetScreenWidth() > m_lastTerrain.GetComponent<EdgeCollider2D>().points[^1].x)
{
GenerateNextTerrain();
}
}
void GenerateNextTerrain()
{
// Instantiate new terrain object
Transform _newTerrain = Instantiate(terrains[m_rnd.Next(0, terrains.Length - 1)], terrainVisualOutput, true);
// Reposition it at the end of the last terrain
_newTerrain.transform.position =
m_lastTerrain.GetComponent<EdgeCollider2D>().points[^1]
- _newTerrain.GetComponent<EdgeCollider2D>().points[0];
// Rename and add to list
_newTerrain.name = $"{m_displayedTerrains.Count}";
m_displayedTerrains.Add(_newTerrain);
}
private float GetScreenWidth()
{
Camera _camera = Camera.main;
float _halfHeight = _camera.orthographicSize;
return _camera.aspect * _halfHeight * 2;
}
}
Related
My game characters keep spawning at one position and they get squashed together i want them to spawn one after another from the door of the shop and after they recieve their product they leave from the door again This is a picture of how they look when they spawn since i'm putting them in a group spawn:
This is the script i'm using to spawn them:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CharacterSpawn : MonoBehaviour
{
[SerializeField]
private float spawnRadius = 7, time = 1.5f;
public GameObject[] enemies;
public Transform groupTransform;
void Start()
{
StartCoroutine(SpawnAnEnemy());
}
IEnumerator SpawnAnEnemy()
{
Vector2 spawnPos = GameObject.Find("Bengal").transform.position;
Instantiate(enemies[Random.Range(0, enemies.Length)], spawnPos, Quaternion.identity, groupTransform);
yield return new WaitForSeconds(time);
StartCoroutine(SpawnAnEnemy());
}
}
EDIT: They still spawn together even after trying the solution bellow check this picture:
I think you will need two scripts for this. One to handle selecting enemy to spawn and another to handle where to place the enemy.
This will pick a random enemy prefab:
public GameObject[] enemies; //Add to enemy prefabs
int spawnEnemy; //Random enemy index
void Start()
{
Pick();
}
void Pick()
{
spawnEnemy = Random.Range(0, enemies.Length);
GameObject Spawn = Instantiate(enemies[spawnEnemy], transform.position, Quaternion.identity);
This will place the enemy prefab in a random position. To avoid overlapping, a float "spaceBetweenEnemies" is introduced.
//Add to your enemy manager. Create an empty game object if you don't have one
//This will place the enemies in random position
public GameObject enemyDistribution;
GameObject spawn;
public int numberOfEnemiesToSpawn;
public float spaceBetweenEnemies = 2; //This is the space between enemies. To avoid overlapping
public float xPos = 5; //x axis
public float yPos = 5; //y axis
void Start()
{
for (int i = 0; i < numberOfEnemiesToSpawn; i++)
{
SpreadItem();
}
}
void SpreadItem()
{
Vector3 ranPos = new Vector3(Random.Range(-xPos, xPos) + spaceBetweenEnemies, Random.Range(-yPos, yPos) + spaceBetweenEnemies, 0f) + transform.position; //z axis is set to zero
spawn = Instantiate(enemyDistribution, ranPos, Quaternion.identity);
}
This may not prevent the enemies from spawning outside the screen parameters. I recommend writing a simple script to attract the enemies towards the centre of the screen of to the player. You can use an "OnTriggerEnter2d" method to check for collision between enemies and to move away from each other. There are good tutorials on "Flocking" on YouTube. You enemies will behave like swarms. This might come in handy for your game.
Hope this helps.
Add a float value called spaceBetweenObjects and add it to the spawnPos
//start with and small value and gradually increase or decrease it until you are happy with the result.
public float spaceBetweenObjects = 1;
Instantiate(enemies[Random.Range(0, enemies.Length)], spawnPos + spaceBetweenObjects, Quaternion.identity, groupTransform);
I have a 2D brick breaker style game in Unity where you get to choose the angle of your shot before each turn with the mouse. I'd like to show an accurate trajectory prediction of what a ball will do while the player is aiming, up to maybe 200 frames of ball movement (for now). This involves potentially bouncing off the top, left, and right walls, and any bricks.
I'm using a physics simulation and line renderer to accomplish this but can't get it to work correctly.
It's supposed to copy the gun, walls, and bricks into a virtual Scene, launch a ball in the angle the player is aiming, simulate 200 frames of movement, and plot those in a line renderer.
Here is an abridged version of the game code, reduced to just focus on the aiming and physics simulation for the 1st turn. When I run it, it simply plots the 200 points of the line renderer in the same coordinates as the gun.
What am I doing wrong? Thank you!
using UnityEngine;
using UnityEngine.SceneManagement;
public class Aim3 : MonoBehaviour
{
private Vector3 gunPosScreenV3, aimPointScreenV3,
aimPointWorldV3, aimDirectionV3;
private Vector2 ballDirection, ballVelocity;
[SerializeField] private GameObject gun, walls, bricks;
Camera cam;
LineRenderer lr;
// For Physics Scene
private int maxPhysicsFrameIterations = 200;
private Scene simulationScene;
private PhysicsScene2D physicsScene;
[SerializeField] private GameObject ballPrefab;
private GameObject ghostObj;
void Start()
{
lr = GetComponent<LineRenderer>();
lr.enabled = true;
cam = Camera.main;
CreatePhysicsScene();
}
void Update()
{
DrawTrajectory();
}
// This is run once to create a duplicate of the game board for the physics simulation
private void CreatePhysicsScene()
{
simulationScene = SceneManager.CreateScene("Simulation", new CreateSceneParameters(LocalPhysicsMode.Physics2D));
physicsScene = simulationScene.GetPhysicsScene2D();
// Copy the gun to the simulated scene
ghostObj = Instantiate(gun.gameObject, gun.transform.position, gun.transform.rotation);
ghostObj.GetComponent<SpriteRenderer>().enabled = false;
SceneManager.MoveGameObjectToScene(ghostObj, simulationScene);
// Copy all the bricks to the simulated scene. These are stored under a parent Game Object that
// is attached in the editor
foreach (Transform obj in bricks.transform)
{
ghostObj = Instantiate(obj.gameObject, obj.position, obj.rotation);
ghostObj.GetComponent<SpriteRenderer>().enabled = false;
SceneManager.MoveGameObjectToScene(ghostObj, simulationScene);
}
// Copy the top, left, and right walls to the simulated scene. These are invisible and don't have sprites.
// These are stored under a parent Game Object that is attached in the editor
foreach (Transform obj in walls.transform)
{
ghostObj = Instantiate(obj.gameObject, obj.position, obj.rotation);
SceneManager.MoveGameObjectToScene(ghostObj, simulationScene);
}
} // CreatePhysicsScene
private void DrawTrajectory()
{
// Get the starting Screen position of the gun from the World position
gunPosScreenV3 = cam.WorldToScreenPoint(gun.transform.position);
gunPosScreenV3.z = 1;
// Get position of mouse in screen units
aimPointScreenV3 = Input.mousePosition;
// Get the position of the mouse in world units for aiming
aimPointWorldV3 = cam.ScreenToWorldPoint(aimPointScreenV3);
aimPointWorldV3.z = 1;
// Calculate the normalized direction of the aim point compared to the gun
aimDirectionV3 = (aimPointScreenV3 - gunPosScreenV3).normalized;
Physics2D.simulationMode = SimulationMode2D.Script;
// Instantiate the ball prefab for the simulation
ghostObj = Instantiate(ballPrefab, gun.transform.position, Quaternion.identity);
// Assign the ball's velocity
ballDirection = new Vector2(aimDirectionV3.x, aimDirectionV3.y);
ballVelocity = ballDirection * 20f;
ghostObj.GetComponent<Rigidbody2D>().velocity = ballVelocity;
SceneManager.MoveGameObjectToScene(ghostObj.gameObject, simulationScene);
lr.positionCount = maxPhysicsFrameIterations;
for (var i = 0; i < maxPhysicsFrameIterations; i++)
{
physicsScene.Simulate(Time.fixedDeltaTime);
//Physics2D.Simulate(Time.fixedDeltaTime);
lr.SetPosition(i, ghostObj.transform.position);
}
Destroy(ghostObj);
Physics2D.simulationMode = SimulationMode2D.Update;
} // DrawTrajectory
}
You use aimPointScreenV3 but it appears to never be initialized/calculated.
Found the answer. Just needed to add the velocity to the ball after moving it to the simulated scene.
SceneManager.MoveGameObjectToScene(ghostObj.gameObject, simulationScene);
ghostObj.GetComponent<Rigidbody2D>().velocity = ballVelocity;
I'm building to WebGL and have disabled the physics system in Package Manager to reduce build size. I'm now in a position where I want to find where on a plane a raycast fired from the cursor, lands. See diagram:
Can I do this without the physics system, or will I be required to include it and the couple of mb it adds to the current build size of 5mb?
You could use a Plane.Raycast
using UnityEngine;
public class GetRaycast : MonoBehaviour
{
Camera camera;
public Transform PlaneTransform;
public Transform Player;
// Start is called before the first frame update
private void Start()
{
camera = Camera.main;
}
// Update is called once per frame
private void Update()
{
if (!Input.GetMouseButtonDown(0)) return;
// you just need any 3 points inside of the plane and provide them
// in clockwise order (so the normal points upwards)
// for the raycast this actually wouldn't matter
var plane = new Plane(PlaneTransform.position, PlaneTransform.position + PlaneTransform.forward, PlaneTransform.position + PlaneTransform.right);
// get ray for mouseposition
var ray = camera.ScreenPointToRay(Input.mousePosition);
// enter will be the distance between the ray origin and the hit plane
if (!plane.Raycast(ray, out var enter)) return;
// GetPoint returns the position along the raycast in
// the given distance from the ray origin
Player.position = ray.GetPoint(enter) + Vector3.up * 0.1f;
}
}
However, a huge disadvantage of this method however is that the generated plane is infinite so the raycast detection doesn't end at the limits of the actual plane so you would have to clamp that "manually" again.
You could do this in this simple example where the plane is flat in the world like
public class GetRaycast : MonoBehaviour
{
Camera camera;
public MeshFilter PlaneTransform;
public Transform Player;
// Start is called before the first frame update
private void Start()
{
camera = Camera.main;
}
// Update is called once per frame
private void Update()
{
if (!Input.GetMouseButtonDown(0)) return;
// you just need any 3 points inside of the plane and provide them
// in clockwise order (so the normal points upwards)
// for the raycast this actually wouldn't matter
var plane = new Plane(PlaneTransform.transform.position, PlaneTransform.transform.position + PlaneTransform.transform.forward, PlaneTransform.transform.position + PlaneTransform.transform.right);
// get ray for mouseposition
var ray = camera.ScreenPointToRay(Input.mousePosition);
// enter will be the distance between the ray origin and the hit plane
if (!plane.Raycast(ray, out var enter)) return;
// GetPoint returns the position along the raycast in
// the given distance from the ray origin
var hitPosition = ray.GetPoint(enter);
// check if hitposition is within the Plane mesh
// mesh.bounds returns the bounding box of the mesh but
// unscaled. lossyScale is the overall scale in the scene
// also taking all parent scales into account
if (hitPosition.x < PlaneTransform.mesh.bounds.min.x * PlaneTransform.transform.lossyScale.x
|| hitPosition.x > PlaneTransform.mesh.bounds.max.x * PlaneTransform.transform.lossyScale.x
|| hitPosition.z < PlaneTransform.mesh.bounds.min.z * PlaneTransform.transform.lossyScale.z
|| hitPosition.z > PlaneTransform.mesh.bounds.max.z * PlaneTransform.transform.lossyScale.z)
{
return;
}
Player.position = hitPosition + Vector3.up * 0.1f;
}
}
Now the player should only get set to the new position if the click was inside of the Plane.
Originally I thought you deactivated the Physics package which is experimental and is more about rigidbody and mass collisions. And not related to Physics.Raycast which is part of the UnityEngine.
After your comment I understand you actually deactivate the Built-In Physics.
Otherwise you still would be able to simply use Physics.Raycast like e.g.
using UnityEngine;
public class GetRaycast : MonoBehaviour
{
Camera camera;
public Transform Player;
// Start is called before the first frame update
private void Start()
{
camera = Camera.main;
}
// Update is called once per frame
private void Update()
{
if (!Input.GetMouseButtonDown(0)) return;
if (!Physics.Raycast(camera.ScreenPointToRay(Input.mousePosition), out var hit)) return;
// I just make it float a little to avoid mesh overlaps
Player.position = hit.point + Vector3.up * 0.1f;
}
}
I'm making my first game where obstacles (which are prefabs) are placed by a script into a scene. They are all different sizes in a 2D environment. I am placing them using this code below
Instantiate(normal1, new Vector3(x, y , 0), Quaternion.identity);
This works perfectly, except that I need all of the obstacles to be positioned from the top left. So if I would have 0, 0 for my x and y, only the obstacle's corner would be covering the 0, 0 position, not the entire thing. From the little I've worked with GUI elements, you can align to however you like. Is this the same with prefab, and if so, how? Or can you somehow move the origin of the object?
I assume you are talking about non-UI elements.
The easiest thing would be to give your objects a parent GameObject and arrange them in a way so the parent GameObject already has the pivot where you want it (the top-left corner). You do this by first positioning the (future) parent object correctly and then simply drag the child object into it in the hierachy (while it keeps its current position).
Then in your script you have to use Camera.ScreenToWorldPoint in order to find the top-left screen corner position in the 3D world like
// an optional offset from the top-left corner in pixels
Vector2 PixelOffsetFromTopLeft;
// Top Left pixel is at
// x = 0
// y = Screen height
// and than add the desired offset
var spawnpos = new Vector2(0 + PixelOffsetFromTopLeft.x, Screen.height - PixelOffsetFromTopLeft.y);
// as z we want a given distance to camera (e.g. 2 Units in front)
spawnedObject = Instantiate(Prefab, camera.ScreenToWorldPoint(new Vector3(spawnpos.x, spawnpos.y, 2f)), Quaternion.identity);
Full script I used as example
public class Spawner : MonoBehaviour
{
public GameObject Prefab;
public Vector2 PixelOffsetFromTopLeft = Vector2.zero;
private GameObject spawnedObject;
private Camera camera;
private void Start()
{
camera = Camera.main;
}
private void Update()
{
if (Input.anyKeyDown)
{
// you don't need this I just didn't want to mess up the scene
// so I just destroy the last spawned object before placing a new one
if (spawnedObject)
{
Destroy(spawnedObject);
}
// Top Left pixel is at
// x = 0
// y = Screen height
// and than add the desired offset
var spawnpos = new Vector2(0 + PixelOffsetFromTopLeft.x, Screen.height - PixelOffsetFromTopLeft.y);
// as z we want a given distance to camera (e.g. 2 Units in front)
spawnedObject = Instantiate(Prefab, camera.ScreenToWorldPoint(new Vector3(spawnpos.x, spawnpos.y, 2f)), Quaternion.identity);
}
}
}
This will now always spawn the object anchored to the top left. It will not keep it this way if the camera moves or the window is resized.
If your question was about keeping that position also when the window is resized you can use the same code e.g. in an Update method (later you due to efficiency you should only call it when really needed / window actually was resized) like
public class StickToTopLeft : MonoBehaviour
{
private Camera camera;
public Vector2 PixelOffsetFromTopLeft = Vector2.zero;
private void Start()
{
camera = Camera.main;
}
// Update is called once per frame
private void Update()
{
var spawnpos = new Vector2(0 + PixelOffsetFromTopLeft.x, Screen.height - PixelOffsetFromTopLeft.y);
// This time simply stay at the current distance to camera
transform.position = camera.ScreenToWorldPoint(new Vector3(spawnpos.x, spawnpos.y, Vector3.Distance(camera.transform.position, transform.position)));
}
}
This now keeps the object always anchored to the top-left corner also after resizing the window and allows to adjust its offset afterwards.
I want to restrict a moving object on a plane( the plane may be or may not be a square) here is the prototype:
I attached a mesh collider to the plane and hope to use that to restrict the movement of the sphere the sphere will move randomly, the idea is that when the ball moved to the edge the velocity will be reflected so it will not fall off the plane with this:
public class BallScript : MonoBehaviour {
// Use this for initialization
void Start () {
GetComponent<Rigidbody>().velocity = new Vector3(0.1f,0,0.1f);
}
private void OnCollisionExit(Collision collision)
{
GetComponent<Rigidbody>().velocity = -GetComponent<Rigidbody>().velocity;
}
}
but the ball kept fall at the edge,
I can't make edge colliders cause the shape of the plane will not be decided until runtime.
Is there a way to do that?
The OnCollisionExit function is called when the object is no longer colliding with the other object. In, your case, it will be called not when the ball is on the edge of the plane but when the ball is no longer touching the plane.
The safest way to do this is to use a BoxCollider. I suggest you just use Unity's Cube primitive which comes with a BoxCollider. The mesh on it will be useful and should act as a visual guide when resizing the BoxCollider.
1.Go to GameObject ---> 3D Object ---> Cube and create new Cube Objects. Looking at the shape of your plane, you will need 6 BoxColliders so this means that you need 6 cubes.
2.Resize, move and rotate each one until you they cover each side of the plane.
3.Create new layer and call it "Boundary". Select each cube and set the layer to "Boundary".
4.Got to Edit ---> Project Settings ---> Physics and use the Layer Collision Matrix to make sure that "Boundary" cannot collide with "Boundary".
5.Disable or remove MeshRenderer of each cube and that's that.
The code in your question is unnecessary but if you still want it to reflect then use OnCollisionEnter and check when the ball hits the wall.
Attach to the ball:
Rigidbody rb;
public float force = 50;
void Start()
{
rb = GetComponent<Rigidbody>();
}
void OnCollisionEnter(Collision col)
{
if (col.collider.CompareTag("Boundary"))
rb.AddForce(col.contacts[0].normal * force, ForceMode.Impulse);
}
So here's just an Idea as the OP agreed to put this as an answer
public Layer[] layerPriorities =
{
Layer.Enemy,
Layer.Walkable
};
[SerializeField] float distanceToBackground = 100f;
public delegate void OnLayerChange(Layer newLayer); // declare new delegate type
//lets use event for the protection of the layerchanges
public event OnLayerChange onlayerChange; // instantiate a observer set
//Look for and return priority layer hit
foreach (Layer layer in layerPriorities)
{
var hit = RaycastForLayer(layer);
if (hit.HasValue)
{
raycastHit = hit.Value;
if(layerHit != layer){ //if layer has changed
layerHit = layer;
onlayerChange(layer); //call the delegate
}
layerHit = layer;
return;
}
}
// otherwise return background hit
raycastHit.distance = distanceToBackground;
layerHit = Layer.RaycastEndStop;
}
//? is a nullable parameter
RaycastHit? RaycastForLayer(Layer layer)
{
/*(Use a bitshift) <<*/
int layerMask = 1 << (int)layer; // lets do masking formation
Ray ray = viewCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit hit; //used as an out parameter
bool hasHit = Physics.Raycast(ray, out hit, distanceToBackground, layerMask);
if (hasHit)
{
return hit;
}
return null;
}
and create an enum for that
public enum Layer
{
Walkable = 8,
RaycastEndStop = -1 //lowest priority
}
As the Layer.Enemy, You could set it to NotWalkable something like well it depends you.
So the small missing part here is just calculate the distance of the ball from the edge of the plane instead of clicking it . That's what I am telling you about the SqrMagnitude