I'm drawing dynamic lines using LineRender in Unity3d. And now I need to delete this lines by clicking right mouse button on line. But I can't get the click on line. Is there any way to do it or I must use something instead of LineRender?
I tried to add to line EdgeCollider and then catch mouse click like
RaycastHit2D hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, 0)), Vector2.zero);
if (hit.collider != null)
{
Destroy(hit.collider.gameObject);
}
But this way I can delete only the last drawn line
Here is the way I create lines:
void Update(){
if (Input.GetMouseButtonDown(0))
{
if (_line == null)
CreateLine(material);
_mousePos = (Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x,
Input.mousePosition.y, -transform.position.z)));
_line.SetPosition(0, _mousePos);
_line.SetPosition(1, _mousePos);
}
else if (Input.GetMouseButtonUp(0) && _line)
{
_mousePos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y,
-transform.position.z));
_line.SetPosition(1, _mousePos);
_line = null;
}
else if (Input.GetMouseButton(0) && _line)
{
_mousePos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y,
-transform.position.z));
_line.SetPosition(1, _mousePos);
}
}
private void CreateLine(Material material)
{
_line = new GameObject("wall" + walls.Count).AddComponent<LineRenderer>();
_line.material = material;
_line.gameObject.AddComponent<EdgeCollider2D>();
_line.positionCount = 2;
_line.startWidth = 0.15f;
_line.endWidth = 0.15f;
_line.numCapVertices = 50;
}
Solution for destroying independent lines
So you want to destroy independent lines by a mouse click. Probably the reason you are having the problem of clicking anywhere and destroying the last line, is that you are leaving EdgeCollider2D with default data, so all lines have the edge collider in the same points; and somehow your Scene and Camera are in a position where no matter where you click, the Raycast2D hits them all, and you delete only one, the first one it hits.
Now the best solution is using the BakeMesh method, but how to implement it correctly.
First, when you create the line instead of _line.gameObject.AddComponent<EdgeCollider2D>(); add a MeshCollider.
Then, after you create the line and let the button up in else if (Input.GetMouseButtonUp(0) && _line) you need to bake the line mesh and attached it to the mesh collider.
else if (Input.GetMouseButtonUp(0) && _line)
{
_mousePos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y,
-transform.position.z));
_line.SetPosition(1, _mousePos);
//Bake the mesh
Mesh lineBakedMesh = new Mesh(); //Create a new Mesh (Empty at the moment)
_line.BakeMesh(lineBakedMesh, true); //Bake the line mesh to our mesh variable
_line.GetComponent<MeshCollider>().sharedMesh = lineBakedMesh; //Set the baked mesh to the MeshCollider
_line.GetComponent<MeshCollider>().convex = true; //You need it convex if the mesh have any kind of holes
_line = null;
}
Also, depending on how your scene is setup you may try making the line not use world space as the default being true, like _line.useWorldSpace = false;
To this point, we should have a LineRenderer with a MeshCollider attached where the MeshCollider is the baked mesh of the LineRenderer.
Lastly, how to destroyed them, it's almost the same as you have previously, but instead of Raycast2d, use the normal 3d method (Physics.Raycast) because MeshCollider is from 3d physics.
Please read more about:
LineRenderer's BakeMesh
MeshCollider
MeshCollider's Convex
MeshCollider's sharedMesh example
Solution if you want just one line
The problem is that you creating new independent lines (LineRenderer) each time, so when you want to delete the full line, you are deleting only one of them and not all.
You can continue doing that way but you need to somehow save each independent line in a list and then delete all the line game objects that complete your whole line.
But the best solution is to use LineRenderer to add more points to your line, every time you want a new point yo should increase your LineRendere's _line.positionCount = line.positionCount + 1; and then save the position to that point, like _line.SetPosition(line.positionCount-1, _mousePos);
Now using BakeMesh (as #Iggy suggested) would work because you have just one LineRenderer with all points, you just need to use the Mesh you got to a MeshCollider, and use it like this example.
EDIT: After reading all the comments and code I understand what is what #vviolka wanted, so I add a new solution.
Related
right now I'm working on a planet generation project in the Unity engine.
The problem I have happens when I want to place plants on the planet surface.
My approach is to cast rays from the center of the planet out towards the surface to get the positions of the plants (because the surface is no plain sphere, so I don't know the exact positions). But I don't get any hits.
So I tried several things to fix that. And what I found is quite strange for me.
When I create a Ray using "new Ray", I get nothing. But with "UnityEngine.Camera.main.ScreenPointToRay" I get some expected hits. So it cannot be an issue about the GameObject layers.
For me it looks like using "new Ray" is buggy or something.
Anyone here to explain this?
public void CreatePlants(List<PlantDefinition> plantSpeciesDefinitions)
{
foreach (PlantDefinition plantDefinition in plantSpeciesDefinitions)
{
directions = new Vector3[plantDefinition.maximumCount];
for (int i = 0; i < plantDefinition.maximumCount; i++)
{
Vector3 direction = new Vector3(Random.Range(-1f, 1f), Random.Range(-1f, 1f), Random.Range(-1f, 1f));
direction.Normalize();
directions[i] = direction;
Ray ray = new Ray(planetCenter, directions[i]); // does not give any hits
Ray ray2 = UnityEngine.Camera.main.ScreenPointToRay(Input.mousePosition); // does give hits
Physics.Raycast(ray2, out RaycastHit hit);
if(hit.collider != null)
Debug.LogWarning($"Object hit: {hit.collider.gameObject.name}");
}
}
}
By default raycasts in Unity do not hit the "inside" / backfaces of a Collider.
You start your ray inside a sphere and want to hit the surface from the inside.
You will have to enable Physics.queriesHitBackfaces either via code
private void Awake ()
{
Physics.queriesHitBackfaces = true;
}
or via the Physics Settings
I have a prefab of a enemy that will spawn in a random position multiple times around the player. However, sometimes this can make one enemy prefab overlap another enemy prefab.
So, I wrote a script which uses Physics2D.OverlapCircleAll() to detect any colliders before instantiating the enemy prefab which avoids the enemy prefab from overlaping an existing enemy. My issue is that the OverlapCircleAll() didn't detect the other instances of the prefab.
I already tried with Physics2D.OverlapBoxAll aswell. If I spawn more than 30 of these "enemy prefabs", at least one will overlap another enemy
This is the code used to detect overlap:
public void SpawnEachEnemy(GameObject Enemy)
{
Vector3 futurePosition = new Vector2(UnityEngine.Random.Range(UpperLeft.transform.position.x, DownRight.transform.position.x),
UnityEngine.Random.Range(UpperLeft.transform.position.y, DownRight.transform.position.y));
bool correctPosition = false;
while (!correctPosition)
{
Collider2D[] collider2Ds = Physics2D.OverlapCircleAll(futurePosition,0.2f);
if (collider2Ds.Length > 0)
{
//re-spawning to prevent overlap
futurePosition = new Vector2(UnityEngine.Random.Range(UpperLeft.transform.position.x, DownRight.transform.position.x),
UnityEngine.Random.Range(UpperLeft.transform.position.y, DownRight.transform.position.y));
}
else
{
correctPosition = true;
}
}
GameObject b = Instantiate(Enemy) as GameObject;
b.transform.position = futurePosition;
b.transform.parent = this.transform;
}
Louis Garczynski mentioned a few of the possibilities but one that wasn't mentioned is that if these are all instantiating in the span of a single frame (a guess based on a comment saying SpawnEachEnemy is called in a loop), then you may need to enable Auto Sync Transforms under Physics2D Settings:
This minimal reproducible example when attached to the camera in a new 3D project's scene should work as you intend with Auto Sync Transforms enabled and it will fail to prevent overlaps when it is disabled. It may be what is preventing it from working for you:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestScript : MonoBehaviour
{
Vector3 upperLeft;
Vector3 downRight;
GameObject prefab;
// Start is called before the first frame update
void Start()
{
transform.position = new Vector3(0, 0, -3);
upperLeft = new Vector3(-1, -1);
downRight = new Vector3(1, 1);
prefab = GameObject.CreatePrimitive(PrimitiveType.Sphere);
DestroyImmediate(prefab.GetComponent<SphereCollider>());
prefab.transform.localScale = 0.4f * Vector3.one;
prefab.AddComponent<CircleCollider2D>();
for (int i = 0; i < 12; i++)
{
SpawnEachEnemy(prefab);
}
prefab.SetActive(false);
}
public void SpawnEachEnemy(GameObject Enemy)
{
Vector3 futurePosition;
Collider2D[] collider2Ds;
do {
futurePosition = new Vector2(
UnityEngine.Random.Range(
upperLeft.x,
downRight.x),
UnityEngine.Random.Range(
upperLeft.y,
downRight.y));
collider2Ds = Physics2D.OverlapCircleAll(futurePosition, 0.2f)
}
while (collider2Ds.Length > 0)
GameObject b = Instantiate(Enemy) as GameObject;
b.transform.position = futurePosition;
b.transform.parent = this.transform;
}
}
I ran into exactly this while writing some feature tests.
As addendum to Ruzihm's awesome answer (was stuck for ages until I found that!).
If your game does not explicitly need AutoSyncTransforms every frame, then its preferable to leave it off, as it can cause a performance hit.
You should only set autoSyncTransforms to true for physics backwards compatibility in existing projects
If you just need it in tests or on a loading frame then:
You can manually call a transform sync with:
Physics.SyncTransforms(); or Physics2D.SyncTransforms();
Or set Physics.autoSyncTransforms = true; at the start, and then back to false at the end of your Start() method.
Either of these is preferable as you don't incur a penalty on subsequent frames.
If your finding you must use AutoSyncTransform or SyncTransform() in normal running. Consider a Coroutine to defer instantiations so that a script isn't creating lots of things at once.
Ideally you want as many frames a second as possible, so there 'may' be little gameplay impact spawning things one/a few at a time, on sequential frames. Rather than incurring an overall performance hit and potential stuttering as a script tries to create too much at once.
First your code could be simplified to something like this:
public void SpawnEachEnemy(GameObject Enemy)
{
Vector3 futurePosition;
do
{
futurePosition = new Vector2(
UnityEngine.Random.Range(UpperLeft.transform.position.x, DownRight.transform.position.x),
UnityEngine.Random.Range(UpperLeft.transform.position.y, DownRight.transform.position.y)
);
} while (Physics2D.OverlapCircleAll(futurePosition,0.2f).Length > 0)
GameObject b = Instantiate(Enemy) as GameObject;
b.transform.position = futurePosition;
b.transform.parent = this.transform;
}
I would also recommend adding a safety counter to your loop, to avoid an infinite loop if there is no room to be found.
Now, a lot of things could be wrong:
Maybe the OverlapCircle and the spawn don't happen in the same place? While setting the parent won't modify the world position, I would still set the position after setting the parent. Not the issue here.
Maybe the size of the overlap is too small ? Are you sure your enemies are 0.2 units in radius? Consider using Debug.DrawLine to draw the radius of the scanned circle.
Maybe your enemies are in a layer not in the DefaultRaycastLayers ? Try using a much larger circle radius and add a Debug.Log whenever OverlapCircleAll actually works.
There are a few other possible reasons, like disabled colliders, or colliders that are too small, etc. This should however cover most likely mistakes.
Thank you, Ruzihm!
I was looking for this answer for days, I had the same problem with my code using Physics2D.OverlapCircle. I am surprised I didn't find anyone who mentions Auto Sync Transforms anywhere else.
if ( Physics2D.OverlapCircle(position, radio) == null) {
GameObject obstacleInst = Instantiate(obstacle, transform);
obstacleInst.transform.position = position;
obstacleInst.transform.localScale = new Vector3(scale, scale, 1);
obstacleInst.transform.rotation = Quaternion.Euler(new Vector3(0, 0, Random.Range(0, 360)));
}
I have a script that lays out prefabs, on a surface, by creating an array of vectors. Each array contain 14 vectors, which produce evenly spaced locations, on a plane.
public List<Vector3> handEastVectorPoints = new List<Vector3>();
public List<Vector3> handSouthVectorPoints = new List<Vector3>();
public List<Vector3> handNorthVectorPoints = new List<Vector3>();
public List<Vector3> handWestVectorPoints = new List<Vector3>();
Then when a prefab is instantiated, my code grabs the correct vector, from the correct array, and add the prefab, like this:
public IEnumerator addTileInHand(GameObject tile, Vector3 v, float time, Vector3 rotationVector, int pos)
{
Quaternion rotation = Quaternion.Euler(rotationVector);
GameObject newTile = Instantiate(tile, v, rotation);
tile.GetComponent<TileController>().canDraw = true;
tile.GetComponent<TileController>().gameStatus = getCurrentGameState();
tile.GetComponent<TileController>().currentTurn = sfs.MySelf.Id;
PlayersHand[pos] = newTile;
yield return new WaitForSeconds(time);
}
When this function is called, its passed a vector, which comes from the above array. The prefab is created, and added to an array of gameObjects.
What I now want to do is allow the player to drag and drop the to various "hot spots", as defined in the first set of arrays (again, their are 14 vectors) and drop them. If there is already a prefab in that position, I would want it to snap to the new position that just opened up. If the tile is dropped anywhere else, it should snap back to its original location. I also just want the object to be dragged either left or right, no need to up and down or up through the Y axis.
Can anyone point me to similar script that might help or provide assistance?
thanks
Edit: Draggable Script Got this working. You can only drag on the X axis, but for my use case, this works.
public class DraggableObject : MonoBehaviour {
private bool dragging = false;
private Vector3 screenPoint;
private Vector3 offset;
private float originalY;
void Start()
{
originalY = this.gameObject.transform.position.y;
}
void OnMouseDown()
{
screenPoint = Camera.main.WorldToScreenPoint(gameObject.transform.position);
offset = gameObject.transform.position - Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, originalY, screenPoint.z));
}
void OnMouseDrag()
{
Vector3 cursorPoint = new Vector3(Input.mousePosition.x, originalY, screenPoint.z);
Vector3 cursorPosition = Camera.main.ScreenToWorldPoint(cursorPoint) + offset;
transform.position = cursorPosition;
dragging = true;
}
void OnMouseUp()
{
dragging = false;
}
}
I have done a similar thing for my game. This is quite easy. Follow the following steps and let me know if you have any further queries:
Write a script (let's say HolderScript ) and add a boolean occupied to it. Add an OnTriggerEnter (A Monobehaviour function) to the script and update the boolean accordingly:
Case 1 - Holder is not occupied - Make boolean to true and disable Colliders if the prefab is dropped there, else do nothing.
Case 2 - Holder is occupied - if occupied prefab is removed then in OnTriggerExit function make boolean to false and enable colliders.
Write a Script (let's say DraggableObject) and set the drag and isDropped boolean accordingly and you can use Lerp function for snapping back.
Attach the holder script to the droppable area and DraggableObject script to the draggable objects.
This should solve your issue.
I'm making a 2D TD game and currently I have made using quill18 tutorials a simple building system. However I'm trying to have it snap on my tiles as I mouse over them.
This is my script applied to the building object
void Update(){
Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
mousePos.z = 0;
Vector2 mp = mousePos;
this.transform.position = mousePos;
Collider2D col = GetComponent<Collider2D> ();
if (GameManager.instance.player.CanAffordCurrentBuilding()
&& !col.IsTouchingLayers(LayerMask.GetMask("NonBuildingLayer"))
&& col.IsTouchingLayers(LayerMask.GetMask("BuildingLayer"))
&& !col.IsTouchingLayers(LayerMask.GetMask("BlockingLayer"))) {
SpriteRenderer[] sprites = GetComponentsInChildren<SpriteRenderer>();
foreach(SpriteRenderer sr in sprites)
sr .color = Color.green;
canPlace = true;
}
else {
SpriteRenderer[] sprites = GetComponentsInChildren<SpriteRenderer>();
foreach(SpriteRenderer sr in sprites)
sr .color = Color.red;
canPlace = false;
}
if (Input.GetMouseButtonDown (0) && canPlace) {
SpriteRenderer[] sprites = GetComponentsInChildren<SpriteRenderer>();
foreach(SpriteRenderer sr in sprites)
sr .color = Color.white;
Destroy(GetComponent<Rigidbody2D>() );
Destroy(this);
GameManager.instance.player.BuildingWasPlaced();
}
What this does is that if touching layer is "BuildingLayer" I will be able to place the object and the object is always following my mouse until I place it. However I want it to only follow my mouse when the touching layer is not BuildingLayer. And when the touching layer is BuildingLayer I want it to snap onto the tile that are closest to the mouse that is building layer. And continuously change snapping tile when I move around the mouse.
How could I accomplish something like this?
I have a list called grid that contains all the building tiles in the game if that helps.
I'm thinking what you could do is add trigger collision boxes to your buildings.
If you are going to drag one, make that one you are dragging have a slightly bigger trigger collision box.
Than check if there is a trigger collision on the object your dragging, if so, snap to a position relative to the object the collision is with. You'd need to check whether the collision is on the bottom, left, top or right.
Hi im creating a 2D platformer, and i want to be able to draw for example lines ingame (playmode) with my cursor (like paint) that can act as walkable terrain. Im pretty new to coding and c# which im using, and im having a really hard time imagining how this can be achieved let alone if its possible? Would appreciate if you guys could give me some ideas and maybe could help me push it in the right direction. Thanks!
EDIT:
So i got got some code now which makes me being able to draw in playmode. The question now is how i can implement a type of collider to this? Maybe each dot can represent a little square or something? How can i go through with it? Heres some code. Thanks.
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class DrawLine : MonoBehaviour
{
private LineRenderer line;
private bool isMousePressed;
private List<Vector3> pointsList;
private Vector3 mousePos;
// Structure for line points
struct myLine
{
public Vector3 StartPoint;
public Vector3 EndPoint;
};
// -----------------------------------
void Awake()
{
// Create line renderer component and set its property
line = gameObject.AddComponent<LineRenderer>();
line.material = new Material(Shader.Find("Particles/Additive"));
line.SetVertexCount(0);
line.SetWidth(0.1f,0.1f);
line.SetColors(Color.green, Color.green);
line.useWorldSpace = true;
isMousePressed = false;
pointsList = new List<Vector3>();
}
// -----------------------------------
void Update ()
{
// If mouse button down, remove old line and set its color to green
if(Input.GetMouseButtonDown(0))
{
isMousePressed = true;
line.SetVertexCount(0);
pointsList.RemoveRange(0,pointsList.Count);
line.SetColors(Color.green, Color.green);
}
else if(Input.GetMouseButtonUp(0))
{
isMousePressed = false;
}
// Drawing line when mouse is moving(presses)
if(isMousePressed)
{
mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
mousePos.z=0;
if (!pointsList.Contains (mousePos))
{
pointsList.Add (mousePos);
line.SetVertexCount (pointsList.Count);
line.SetPosition (pointsList.Count - 1, (Vector3)pointsList [pointsList.Count - 1]);
}
}
}
}
Regarding means and tools:
Input.mousePosition
Input.GetKey, Input.GetKeyDown, Input.GetKeyUp
Line renderer manual
Line renderer scripting API
Regarding general idea:
Your script may use Input.GetKey to trigger the feature functionality (keyboard key, for example).
When the feature is activated the script awaits of mouse button to be clicked by the means of Input.GetKeyUp and when the event happens it must capture current mouse position using Input.mousePosition.
Only two points are necessary to build a segment of line, so when the script detects input of the second point it may create game object that will represent a piece of walkable terrain.
Visually such an object may be represented with line renderer. To enable iteraction with other game objects (like player character) it is usually enough to enhance object with a collider component (presumably, with a BoxCollider).
Regarding of how-to-add-a-collider:
GameObject CreateLineCollider(Vector3 point1, Vector3 point2, float width)
{
GameObject obj = new GameObject("LineCollider");
obj.transform.position = (point1+point2)/2;
obj.transform.right = (point2-point1).normalized;
BoxCollider boxCollider = obj.AddComponent<BoxCollider>();
boxCollider.size = new Vector3( (point2-point1).magnitude, width, width );
return obj;
}
You can add collider to object with line renderer but you still must orient it properly.
Example of integration in your code:
void Update ()
{
...
// Drawing line when mouse is moving(presses)
if(isMousePressed)
{
mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
mousePos.z=0;
if (!pointsList.Contains (mousePos))
{
pointsList.Add (mousePos);
line.SetVertexCount (pointsList.Count);
line.SetPosition (pointsList.Count - 1, (Vector3)pointsList [pointsList.Count - 1]);
const float ColliderWidth = 0.1f;
Vector3 point1 = pointsList[pointsList.Count - 2];
Vector3 point2 = pointsList[pointsList.Count - 1];
GameObject obj = new GameObject("SubCollider");
obj.transform.position = (point1+point2)/2;
obj.transform.right = (point2-point1).normalized;
BoxCollider boxCollider = obj.AddComponent<BoxCollider>();
boxCollider.size = new Vector3( (point2-point1).magnitude, ColliderWidth , ColliderWidth );
obj.transform.parent = this.transform;
}
}
}