I am building a 3d game in Unity3D, where the use can draw on the ground when he is in top view.
The lines are drawn using the LineRenderer component.
I want to give the use the ability to select the lines he draw so he can delete them. But unfortunately I didn't find a good way to do it. If I am using 3D colliders, the only one that fits is a BoxCollider, but it's too big (I want the collider only on the line).
And I can't use the 2D colliders because they only work on XY plane.
I tried to convert the line into a mesh and just use MeshCollider but the line was too complex and the MeshCollider couldn't fit it self properly on the line.
Do you have any idea how can I do it¿
P.S.
I am selecting objects in the game with Ray casts.
if a MeshCollider is too complex, but a BoxCollider is not precise enough, try using multiple BoxColliders along your line segments.
An implementation can be found here:
https://answers.unity.com/questions/1546512/detect-mouse-click-on-line-renderer.html
If the link ever breaks, here is the code:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
[RequireComponent(typeof(LineRenderer))]
public class LineCollider : MonoBehaviour
{
LineRenderer line;
public void Start() {
AddCollider(6); //Increase the count of parts(6) if you want to get more detailed collider
}
public void AddCollider(int part)
{
try
{
line = GetComponent<LineRenderer>();
var start = line.GetPosition(0);
var end = line.GetPosition(line.positionCount - 1);
var a = (line.positionCount - 1) / part;
for (int i = 1; i<=part; i++)
{
if (i == 1)
AddColliderToLine(start, line.GetPosition(Mathf.CeilToInt(a * 1)));
else if (i == part)
AddColliderToLine(line.GetPosition(Mathf.CeilToInt(a * (i - 1))), end);
else
AddColliderToLine(line.GetPosition(Mathf.CeilToInt(a * (i - 1))), line.GetPosition(Mathf.CeilToInt(a * i)));
}
}
catch
{
Destroy(gameObject);
}
}
private void AddColliderToLine(Vector3 start, Vector3 end)
{
var startPos = start;
var endPos = end;
BoxCollider col = new GameObject("Collider").AddComponent<BoxCollider>();
col.transform.parent = line.transform;
float lineLength = Vector3.Distance(startPos, endPos);
col.size = new Vector3(lineLength, 0.175f, 0.25f);
Vector3 midPoint = (startPos + endPos) / 2;
col.transform.position = midPoint;
float angle = (Mathf.Abs(startPos.y - endPos.y) / Mathf.Abs(startPos.x - endPos.x));
if ((startPos.y < endPos.y && startPos.x > endPos.x) || (endPos.y < startPos.y && endPos.x > startPos.x))
{
angle *= -1;
}
angle = Mathf.Rad2Deg * Mathf.Atan(angle);
col.transform.Rotate(0, 0, angle);
}
}
As an alternative, more performant but harder to implement: Write the lines into a texture and sample the pixels you clicked - each line should have a distinct color. This also assures you have some z-order when lines are drawn overlapping.
See my answer here to get an idea: How does Java / OS detect click co-ordinates on a viewport?
Related
In my 2D game, the player Prefab has children which are weapons (like swords) so that when the player moves, the weapons translate in world space with his movements while maintaining a constant local position (until of course it is time to attack). The weapons automatically point towards nearby enemies, and swing when they get close enough.
I want the weapon to follow a swing arc by rotating around a pivot point defined at half of the weapon's range in the direction of the enemy. Once the weapon starts the swing, the arc's local position and rotation should remain unchanged and no longer care about the enemy position while the world arc will obviously translate with the player. The weapon should follow this arc purely relative to the player.
RotateAround seems to only work in world space, and thus causes strange problems when trying to rotate around an object in world space while the weapon's world position (as well as my desired pivot point's world position) would be translating with the player. Also, the point that I need it to rotate around needs to be relative to the local space, since when the player moves, the weapon needs to maintain its local arc while also translating with the player.
I also tried using Vector3.Slerp on the weapon's transform.localPosition, which seemed like it would be the perfect solution, but I can't seem to get the arc to match what I envision a good round swing would look like.
The attack consists of three parts: Backswing, Foreswing, and Recovery. The part that I care most about is the foreswing arc, as the others can be acheived easily with simply local rotations and lerping.
const int BACKSWING = 0;
const int FORESWING = 1;
const int RECOVER = 2;
float[] timeFrac = { .15f, .25f, .6f };
float[] rotations = { 120f, -240f, 120f };
float backSwingDistMultiplier = .5f;
//Swing Attack
public override IEnumerator Attack(float startAngle) {
var totalAttackTime = GetAttackTime();
var backSwingDist = backSwingDistMultiplier * Range;
var startPos = transform.localPosition;
var slerpCenterDiff = PerpDir(dir).normalized;
//Interpolation arrays
float[] swingTimes = { timeFrac[BACKSWING] * totalAttackTime,
timeFrac[FORESWING] * totalAttackTime,
timeFrac[RECOVER] * totalAttackTime };
float[] startAngles = { startAngle,
startAngle + rotations[BACKSWING],
startAngle + rotations[BACKSWING] + rotations[FORESWING] };
Vector3[] swingPositions = { startPos - (dir - slerpCenterDiff ) * backSwingDist,
startPos + dir * Range + slerpCenterDiff * backSwingDist };
Vector3[] slerpCenters = { (startPos + swingPositions[BACKSWING]) * .5f + slerpCenterDiff ,
((swingPositions[BACKSWING] + swingPositions[FORESWING]) * .5f) + slerpCenterDiff };
Vector3[] slerpStarts = { startPos - slerpCenters[BACKSWING],
swingPositions[BACKSWING] - slerpCenters[FORESWING]};
Vector3[] slerpEnds = { swingPositions[BACKSWING] - slerpCenters[BACKSWING],
swingPositions[FORESWING] - slerpCenters[FORESWING]};
timer = 0;
float percentDone;
//A swing attack has backswing, foreswing, and recovery
for (int swing = 0; swing <= 2; swing++) {
while (timer < swingTimes[swing]) {
percentDone = timer / swingTimes[swing];
//Backswing and Foreswing will slerp
if (swing < RECOVER) {
transform.localPosition = Vector3.Slerp(slerpStarts[swing], slerpEnds[swing], percentDone);
transform.localPosition += slerpCenters[swing];
} else { //Recover will lerp
transform.localPosition = Vector3.Lerp(swingPositions[FORESWING], startPos, percentDone);
}
transform.localRotation = Quaternion.Euler(0, 0,
startAngles[swing] + rotations[swing] * percentDone);
timer += Time.deltaTime;
yield return null;
}
transform.localRotation = Quaternion.Euler(0, 0, startAngles[swing] + rotations[swing]);
timer -= swingTimes[swing];
}
transform.localPosition = startPos;
}
I would just make an animation out of it, but I need the range to be dynamic, which is next to impossible to achieve with keyframes.
I was able to get my desired outcome by defining the arc centers in localPosition first:
var backswingArcCenter = (startPos + swingPositions[BACKSWING]) * 0.5f;
var foreswingArcCenter = (swingPositions[BACKSWING] + swingPositions[FORESWING]) * 0.5f;
and then calling RotateAround using that position added to the player's world space
if (swing == FORESWING) {
transform.RotateAround(player.transform.position + foreswingArcCenter,
Vector3.forward, Time.deltaTime / swingTimes[swing] * 270f);
} else if (swing == BACKSWING) {
transform.RotateAround(player.transform.position + backswingArcCenter,
Vector3.forward, Time.deltaTime / swingTimes[swing] * -180f);
}
I am developing a FPS game in Unity and I am wanting to have an agent (an animated bat) strafe around a target.
Everything works fine, but the bat is facing a fixed direction instead of facing the direction of travel as it strafes around the target.
I have tried adding a Quaternion rotation as per the code listed below (see 'Face in the right direction') but to no avail. I have commented out sections of code to see if I can identify where the issue lies, but that just cause more issues.
Any help would be greatly appreciated.
Code...
// Target position. The gameobject will circle around this position.
[SerializeField]
private Vector3 TargetPosition = new Vector3();
// Temp Position. Used to generate a new position on the circle.
private Vector3 tempPosition = new Vector3();
// Radius. Generated from the game objects current distance to the target.
float r;
// The current angle in the circle strafe.
float theta;
/// movement variables
private Vector3 currentTargetPos;
// Start is called before the first frame update
void Start()
{
// Get the radius. The distance to the target position.
r = Vector3.Distance(transform.position, TargetPosition);
// Default theta.
theta = Mathf.Acos((transform.position.x - TargetPosition.x) / r);
// Check quadrant and adjust.
if ((transform.position.x - TargetPosition.x) > 0 &&
(transform.position.y - TargetPosition.y) > 0)
{
Debug.Log("Quadrant 1");
}
if ((transform.position.x - TargetPosition.x) < 0 &&
(transform.position.y - TargetPosition.y) > 0)
{
Debug.Log("Quadrant 2");
}
if ((transform.position.x - TargetPosition.x) < 0 &&
(transform.position.y - TargetPosition.y) < 0)
{
Debug.Log("Quadrant 3");
theta = -theta;
}
if ((transform.position.x - TargetPosition.x) > 0 &&
(transform.position.y - TargetPosition.y) < 0)
{
Debug.Log("Quadrant 4");
theta = -theta;
}
Debug.Log("theta x = " + theta);
}
// Update is called once per frame
void Update()
{
// Get the actors circle strafe location.
tempPosition.x = TargetPosition.x + r * Mathf.Cos(theta);
tempPosition.z = TargetPosition.z + r * Mathf.Sin(theta);
tempPosition.y = transform.position.y;
// Update t.
theta += 0.2f * Time.deltaTime;
// Set the location.
transform.position = tempPosition;
// clear y to avoid up/down movement
currentTargetPos.y = transform.position.y;
Vector3 direction = currentTargetPos - transform.position;
// face in the right direction
direction.y = 0;
Quaternion rotation = Quaternion.LookRotation(-direction, Vector3.up);
transform.rotation = rotation;
}
// Draws debug objects in the editor and during editor play (if option set).
void OnDrawGizmos()
{
// Draw target position.
Gizmos.color = Color.red;
Gizmos.DrawSphere(TargetPosition, 0.2f);
}
}
Unity has a built-in function for transforms called LookAt() that takes a Transform other or Vector3 worldPosition as a parameter. To implement it, you can write transform.LookAt(TargetPosition).
Note that this may impact translations that rely on local rotation, so if you have problems you’ll need to change some logic as well. To circumvent this you may want to reset the rotation before moving, then moving the rotation back to where it needs to be, or better by using world space instead of local space.
In my code, I create 2 matrices that store information about two different meshes, using the following code:
Mesh mesh;
MeshFilter filter;
public SphereMesh SphereMesh;
public CubeMesh CubeMesh;
public Material pointMaterial;
public Mesh pointMesh;
public List<Matrix4x4> matrices1 = new List<Matrix4x4>();
public List<Matrix4x4> matrices2 = new List<Matrix4x4>();
[Space]
public float normalOffset = 0f;
public float globalScale = 0.1f;
public Vector3 scale = Vector3.one;
public int matricesNumber = 1; //determines which matrices to store info in
public void StorePoints()
{
Vector3[] vertices = mesh.vertices;
Vector3[] normals = mesh.normals;
Vector3 scaleVector = scale * globalScale;
// Initialize chunk indexes.
int startIndex = 0;
int endIndex = Mathf.Min(1023, mesh.vertexCount);
int pointCount = 0;
while (pointCount < mesh.vertexCount)
{
// Create points for the current chunk.
for (int i = startIndex; i < endIndex; i++)
{
var position = transform.position + transform.rotation * vertices[i] + transform.rotation * (normals[i].normalized * normalOffset);
var rotation = Quaternion.identity;
pointCount++;
if (matricesNumber == 1)
{
matrices1.Add(Matrix4x4.TRS(position, rotation, scaleVector));
}
if (matricesNumber == 2)
{
matrices2.Add(Matrix4x4.TRS(position, rotation, scaleVector));
}
rotation = transform.rotation * Quaternion.LookRotation(normals[i]);
}
// Modify start and end index to the range of the next chunk.
startIndex = endIndex;
endIndex = Mathf.Min(startIndex + 1023, mesh.vertexCount);
}
}
The GameObject starts as a Cube mesh, and this code stores the info about the mesh in matrices1. Elsewhere in code not shown, I have it so that the Mesh changes to a Sphere and then changes matricesnumber to 2, then triggers the code above to store the info of the new Sphere mesh in matrices2.
This seems to be working, as I'm able to use code like Graphics.DrawMesh(pointMesh, matrices1[i], pointMaterial, 0);
to draw 1 mesh per vertex of the Cube mesh. And I can use that same line (but with matrices2[i]) to draw 1 mesh per vertex of the Sphere mesh.
The Question: How would I draw a mesh for each vertex of the Cube mesh (info stored in matrices1) on the screen and then make those vertex meshes Lerp into the positions of the Sphere mesh's (info stored in matrices2) vertices?
I'm trying to hack at it with things like
float t = Mathf.Clamp((Time.time % 2f) / 2f, 0f, 1f);
matrices1.position.Lerp(matrices1.position, matrices2.position, t);
but obviously this is invalid code. What might be the solution? Thanks.
Matrix4x4 is usually only used in special cases for directly calculating transformation matrices.
You rarely use matrices in scripts; most often using Vector3s, Quaternions and functionality of Transform class is more straightforward. Plain matrices are used in special cases like setting up nonstandard camera projection.
In your case it seems to me you actually only need to somehow store and access position, rotation and scale.
I would suggest to not use Matrix4x4 at all but rather something simple like e.g.
// The Serializable makes them actually appear in the Inspector
// which might be very helpful in you case
[Serializable]
public class TransformData
{
public Vector3 position;
public Quaternion rotation;
public Vector3 scale;
}
Then with
public List<TransformData> matrices1 = new List<TransformData>();
public List<TransformData> matrices2 = new List<TransformData>();
you could simply lerp over
var lerpedPosition = Vector3.Lerp(matrices1[index].position, matrices2[index].position, lerpFactor);
And if you then need it as a Matrix4x4 you can still create it ad-hoc like e.g.
var lerpedMatrix = Matrix4x4.Translation(lerpedPosition);
or however you want to use the values.
Unity has a function Terrain.sampleHeight(point) which is great, it instantly gives you the height of the Terrain underfoot rather than having to cast.
However, any non-trivial project has more than one Terrain. (Indeed any physically large scene inevitably features terrain stitching, one way or another.)
Unity has a function Terrain.activeTerrain which - I'm not making this up - gives you: the "first one loaded"
Obviously that is completely useless.
Is fact, is there a fast way to get the Terrain "under you"? You can then use the fast function .sampleHeight ?
{Please note, of course, you could ... cast to find a Terrain under you! But you would then have your altitude so there's no need to worry about .sampleHeight !}
In short is there a matching function to use with sampleHeight which lets that function know which Terrain to use for a given xyz?
(Or indeed, is sampleHeight just a fairly useless demo function, usable only in demos with one Terrain?)
Is there in fact a fast way to get the Terrain "under you" - so as to
then use the fast function .sampleHeight ?
Yes, it can be done.
(Or indeed, is sampleHeight just a fairly useless demo function,
usable only in demos with one Terrain?)
No
There is Terrain.activeTerrain which returns the main terrain in the scene. There is also Terrain.activeTerrains (notice the "s" at the end) which returns active terrains in the scene.
Obtain the terrains with Terrain.activeTerrains which returns Terrain array then use Terrain.GetPosition function to obtain its position. Get the current terrain by finding the closest terrain from the player's position. You can do this by sorting the terrain position, using Vector3.Distance or Vector3.sqrMagnitude (faster).
Terrain GetClosestCurrentTerrain(Vector3 playerPos)
{
//Get all terrain
Terrain[] terrains = Terrain.activeTerrains;
//Make sure that terrains length is ok
if (terrains.Length == 0)
return null;
//If just one, return that one terrain
if (terrains.Length == 1)
return terrains[0];
//Get the closest one to the player
float lowDist = (terrains[0].GetPosition() - playerPos).sqrMagnitude;
var terrainIndex = 0;
for (int i = 1; i < terrains.Length; i++)
{
Terrain terrain = terrains[i];
Vector3 terrainPos = terrain.GetPosition();
//Find the distance and check if it is lower than the last one then store it
var dist = (terrainPos - playerPos).sqrMagnitude;
if (dist < lowDist)
{
lowDist = dist;
terrainIndex = i;
}
}
return terrains[terrainIndex];
}
USAGE:
Assuming that the player's position is transform.position:
//Get the current terrain
Terrain terrain = GetClosestCurrentTerrain(transform.position);
Vector3 point = new Vector3(0, 0, 0);
//Can now use SampleHeight
float yHeight = terrain.SampleHeight(point);
While it's possible to do it with Terrain.SampleHeight, this can be simplified with a simple raycast from the player's position down to the Terrain.
Vector3 SampleHeightWithRaycast(Vector3 playerPos)
{
float groundDistOffset = 2f;
RaycastHit hit;
//Raycast down to terrain
if (Physics.Raycast(playerPos, -Vector3.up, out hit))
{
//Get y position
playerPos.y = (hit.point + Vector3.up * groundDistOffset).y;
}
return playerPos;
}
Terrain.GetPosition() = Terrain.transform.position = position in world
working method:
Terrain[] _terrains = Terrain.activeTerrains;
int GetClosestCurrentTerrain(Vector3 playerPos)
{
//Get the closest one to the player
var center = new Vector3(_terrains[0].transform.position.x + _terrains[0].terrainData.size.x / 2, playerPos.y, _terrains[0].transform.position.z + _terrains[0].terrainData.size.z / 2);
float lowDist = (center - playerPos).sqrMagnitude;
var terrainIndex = 0;
for (int i = 0; i < _terrains.Length; i++)
{
center = new Vector3(_terrains[i].transform.position.x + _terrains[i].terrainData.size.x / 2, playerPos.y, _terrains[i].transform.position.z + _terrains[i].terrainData.size.z / 2);
//Find the distance and check if it is lower than the last one then store it
var dist = (center - playerPos).sqrMagnitude;
if (dist < lowDist)
{
lowDist = dist;
terrainIndex = i;
}
}
return terrainIndex;
}
It turns out the answer is simply NO, Unity does not provide such a function.
You can use this function to get the Closest Terrain to your current Position:
int GetClosestTerrain(Vector3 CheckPos)
{
int terrainIndex = 0;
float lowDist = float.MaxValue;
for (int i = 0; i < _terrains.Length; i++)
{
var center = new Vector3(_terrains[i].transform.position.x + _terrains[i].terrainData.size.x / 2, CheckPos.y, _terrains[i].transform.position.z + _terrains[i].terrainData.size.z / 2);
float dist = Vector3.Distance(center, CheckPos);
if (dist < lowDist)
{
lowDist = dist;
terrainIndex = i;
}
}
return terrainIndex;
}
and then you can use the function like this:
private Terrain[] _terrains;
void Start()
{
_terrains = Terrain.activeTerrains;
Vector3 start_pos = Vector3.zero;
start_pos.y = _terrains[GetClosestTerrain(start_pos)].SampleHeight(start_pos);
}
public static Terrain GetClosestTerrain(Vector3 position)
{
return Terrain.activeTerrains.OrderBy(x =>
{
var terrainPosition = x.transform.position;
var terrainSize = x.terrainData.size * 0.5f;
var terrainCenter = new Vector3(terrainPosition.x + terrainSize.x, position.y, terrainPosition.z + terrainSize.z);
return Vector3.Distance(terrainCenter, position);
}).First();
}
Raycast solution: (this was not asked, but for those looking for Solution using Raycast)
Raycast down from Player, ignore everything that has not Layer of "Terrain" (Layer can be easily set in inspector).
Code:
void Update() {
// Put this on Player! Raycast's down (raylength=10f), if we hit something, check if the Layers name is "Terrain", if yes, return its instanceID
RaycastHit hit;
if (Physics.Raycast (transform.localPosition, transform.TransformDirection (Vector3.down), out hit, 10f, 1 << LayerMask.NameToLayer("Terrain"))) {
Debug.Log(hit.transform.gameObject.GetInstanceID());
}
}
At this point already, you have a reference to the Terrain by "hit.transform.gameObject".
For my case, i wanted to reference this terrain by its instanceID:
// any other script
public static UnityEngine.Object FindObjectFromInstanceID(int goID) {
return (UnityEngine.Object)typeof(UnityEngine.Object)
.GetMethod("FindObjectFromInstanceID", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)
.Invoke(null, new object[] { goID });
}
But as written above, if you want the Terrain itself (as Terrain object) and not the instanceID, then "hit.transform.gameObject" will give you the reference already.
Input and code snippets taken from these links:
https://answers.unity.com/questions/1164722/raycast-ignore-layers-except.html
https://answers.unity.com/questions/34929/how-to-find-object-using-instance-id-taken-from-ge.html
I want to set the camera 7.5f units away from a player based on where the mouse is in relation to the player.
Warning: Before you answer the question or leave a reply, I ask that you view the picture that I put towards the end of the question so you have an idea of what I am asking for. It is a diagram in which "Point A" is where the camera should be. It is a "for instance," meaning it is an example of what could be an outcome of the mouse, player, and camera.
I found the distance between the mouse and the player:
//Distance: mouse -> player
D1 = Mathf.Sqrt(Mathf.Pow(mousePos.x - player.transform.position.x, 2)
+ Mathf.Pow(mousePos.z - player.transform.position.z, 2));
I set up the statement that will find the relation between the mouse and the player:
if(mousePos.x - player.transform.position.x >= 0)
{
CamX = //Put Code Here
}
if (mousePos.x - player.transform.position.x <= 0)
{
CamX = //Put code Here
}
if (mousePos.z - player.transform.position.z >= 0)
{
CamZ = //Put Code Here
}
if (mousePos.z - player.transform.position.z <= 0)
{
CamZ = //Put Code Here
}
Here are the variables that I am using:
private float D1;
private float D2;
private float D3;
private float CamX;
private float CamZ;
Here is the section of code that I am working on. Don't take it as my full work for it is merely a brainstorm of code:
ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit))
{
mousePos = new Vector3(hit.point.x, 0.8f, hit.point.z);
//Distance: mouse -> player
D1 = Mathf.Sqrt(Mathf.Pow(mousePos.x - player.transform.position.x, 2)
+ Mathf.Pow(mousePos.z - player.transform.position.z, 2));
if(mousePos.x - player.transform.position.x >= 0)
{
CamX = //Put Code Here
}
if (mousePos.x - player.transform.position.x <= 0)
{
CamX = //Put code Here
}
if (mousePos.z - player.transform.position.z >= 0)
{
CamZ = //Put Code Here
}
if (mousePos.z - player.transform.position.z <= 0)
{
CamZ = //Put Code Here
}
//Distance: mouse -> camera
D2 = D1 + 7.5f;
//Distance: player -> camera
D3 = Mathf.Sqrt(Mathf.Pow(player.transform.position.x - CamX, 2)
+ Mathf.Pow(player.transform.position.z - CamZ, 2));
...
My Diagram
Here is a picture diagraming how I want everything to be positioned:
My question is this:
How can I find the position of the camera (point(CamX, CamZ)) that is 7.5f units away from the player and passes through the coordinates of the mouse (point(mousePosX, mousePosZ)).
Couple different ways you could go about this, but one way would be to get the direction vector from the mouse to the player position, and then add that onto your player position multiplied by 7.5
Some example code here:
// assuming Y is the same for player and mouse
Vector3 normDirection = ((Vector3)playerPos - (Vector3)mousePos).normalized; // unit vector from mouse to player
Vector2 extendedDir = new Vector2(normDirection.x * 7.5f, normDirection.z * 7.5f);
Vector2 cameraPos = new Vector2(playerPos.x + extendedDir.x, playerPos.z + extendedDir.y); // extended vector added to player pos
Camera.main.transform.position = new Vector3(cameraPos.x, .8f, cameraPos.y); // set camera position