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.
Related
I'm trying to detect when the mouse clicks on a specific tile on a tile map. The game I'm making is a overhead 2d style, and I want to trigger code when a specific resource node (tile) is clicked on. I've tried to use RaycastHit2D:
public class DetectClickOnTile : MonoBehaviour
{
Vector2 worldPoint;
RaycastHit2D hit;
// Update is called once per frame
void Update()
{
worldPoint = Camera.main.ScreenToWorldPoint(Input.mousePosition);
if (Input.GetMouseButtonDown(0))
{
hit = Physics2D.Raycast(worldPoint, Vector2.down);
if (hit.collider != null)
{
Debug.Log("click on " + hit.collider.name);
Debug.Log(hit.point);
}
}
}
But it only detects collision with the tilemap itself and not any of the individual tiles. Does anyone know why that is? For reference this is the map im trying to detect collision on, only the black tiles are filled in with placeholders, the rest of the tilemap is left blank for now.
Afaik you can get the tile at the given world hit point using something like e.g.
// Reference this vis the Inspector
[SerializeField] TileMap tileMap;
...
hit = Physics2D.Raycast(worldPoint, Vector2.down);
if (hit.collider != null)
{
Debug.Log("click on " + hit.collider.name);
Debug.Log(hit.point);
var tpos = tileMap.WorldToCell(hit.point);
// Try to get a tile from cell position
var tile = tileMap.GetTile(tpos);
...
}
However, I'm pretty sure your Raycast is not what you want to use but rather directly try and get the tile from the worldPoint and if you got one you hit one ;)
Your Raycast goes down meaning you cast from the click point to the bottom of the screen which doesn't sound like what you are trying to achieve.
So probably rather something like
if (Input.GetMouseButtonDown(0))
{
worldPoint = Camera.main.ScreenToWorldPoint(Input.mousePosition);
var tpos = tileMap.WorldToCell(worldPoint);
// Try to get a tile from cell position
var tile = tileMap.GetTile(tpos);
if(tile)
{
...
}
}
Building an RTS building placement system in Unity and I have run into a small problem. I am using the mouseposition and a sphere cast in a state machine to place buildings. When in the "Placing" state, the building prefab follows the mouse position until the player clicks the left mouse button to place the building. If the player is then placing another building, i am using colliders on the already placed building to detect if a building is already there and turning the placing building to red color (for testing purposes). The issue i have run into is the building that is being placed rotates around the building it collides with, and moves over the top of it. How do i stop rotation and y axis movement on an object that is part of a mouse position raycast? I have already tried to constrain movement and rotation in the inspector on the RigidBody, i have tried completely moving the RigidBody altogther, and cant quite figure it out. Would it work if i set the y position and the rotation of the placing object at start and then if the rotation changes or the y position changes, set it back to the starting values? Or is there an easier way to do it?
Neil
private void MoveCurrentPlaceableObjectToMouse()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo;
if(Physics.SphereCast(ray, 2f, out hitInfo))
{
if (hitInfo.collider.tag == "Building")
{
isOkayToBuild = false;
currentPlaceableObject.GetComponentInChildren<MeshRenderer>().material.color = Color.red;
}
else
{
isOkayToBuild = true;
currentPlaceableObject.GetComponentInChildren<MeshRenderer>().material.color = Color.white;
}
currentPlaceableObject.GetComponent<BoxCollider>().isTrigger = false;
state = BuildingState.Placing;
currentPlaceableObject.transform.position = hitInfo.point;
currentPlaceableObject.transform.rotation = Quaternion.FromToRotation(Vector3.up, hitInfo.normal);
}
}
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.
I have a script that is applied to several spheres in my scene. I would like to be able to move individual spheres around in the scene by holding them on the screen and dragging around. My first issue is that no matter which sphere you touch and drag, all the spheres move together (they are part of the same prefab but none of them are children of the other spheres). A second issue is that when you touch and drag anywhere on the screen the spheres react, instead of only reacting when you touch on one of the spheres.
Touch touch = Input.GetTouch(0);
for (int i = 0; i < Input.touchCount; i++)
{
if (touch.phase == TouchPhase.Began)
{
screenPoint = Camera.main.WorldToScreenPoint(gameObject.transform.position);
offset = gameObject.transform.position - Camera.main.ScreenToWorldPoint(new Vector3(touch.position.x, touch.position.y, screenPoint.z));
}
else if (touch.phase == TouchPhase.Moved)
{
Vector3 cursorPoint = new Vector3(touch.position.x, touch.position.y, screenPoint.z);
Vector3 cursorPosition = Camera.main.ScreenToWorldPoint(cursorPoint) + offset;
transform.position = cursorPosition;
}
}
I have a feeling that the issue of all the spheres moving together stems from the fact that the same script is applied to all the spheres, and screenpoint and offest are being derived from gameObject.transform.position, instead of the specific sphere that was touched. I have tried to fix this by putting the following code above the for loop, but I am guessing this might not even be the correct approach. I am new to this :).
RaycastHit hit = new RaycastHit();
Physics.Raycast(touch.position, touch.position, out hit);
if (hit.collider == GameObject.FindGameObjectWithTag("Sphere0"))
{
selectedObject = GameObject.FindGameObjectWithTag("Sphere0");
}
I have my tags set up properly, and if that worked for the first sphere (Sphere0) I would have written the code for the other spheres. But, it seems to not work at all. My project is an ARCore project, but I don't think that should change what I need to code for these aspects of the scene.
Any and all help is greatly appreciated!
You will just want 1 master script separate from you spheres to do the raycasting on touch and sphere controlling. Then tag all your spheres with the same tag (ie. "sphere").
void Update()
{
Touch touch = Input.GetTouch(0);
if (touch.phase == TouchPhase.Began) // when screen is touched...
{
RaycastHit hit;
if (Physics.Raycast(Camera.main.ScreenToWorldPoint(touch.position), Vector3.forward, out hit)) // ...cast a ray...
{
if (hit.collider.tag == "sphere") //...and check if ray hits a sphere
{
selectedObject = hit.collider.gameObject;
}
}
}
// add touch controls here and apply to selectedObject for movement
}
I am new to Unity and I am trying to figure out how to move the camera over a map/terrain using touch input. The camera would be looking down at the terrain with a rotation of (90,0,0). The terrain is on layer 8. I have had no problem getting it moving with keyboard, now I am trying to move to touch and it is very different if you want to keep expected usage on iOS.
The best example I can think of on a built in iOS app is Maps where the user would touch the screen and that point on the map would stay under the finger as long as the finger stayed on the screen. So as the user moves their finger the map appears to be moving with the finger. I have not been able to find examples that show how to do it this way. I have seen may examples of moving the camera or character with the mouse but they don't seem to translate well to this style.
Also posted on Unity3D Answers:
http://answers.unity3d.com/questions/283159/move-camera-over-terrain-using-touch-input.html
Below should be what you need. Note that it's tricky to get a 1 to 1 correspondence between finger/cursor and the terrain when using a perspective camera. If you change your camera to orthographic, the script below should give you a perfect map between finger/cursor position and map movement. With perspective you'll notice a slight offset.
You could also do this with ray tracing but I've found that route to be sloppy and not as intuitive.
Camera settings for testing (values are pulled from the inspector so apply them there):
Position: 0,20,0
Orientation: 90,0,0
Projection: Perspective/Orthographic
using UnityEngine;
using System.Collections;
public class ViewDrag : MonoBehaviour {
Vector3 hit_position = Vector3.zero;
Vector3 current_position = Vector3.zero;
Vector3 camera_position = Vector3.zero;
float z = 0.0f;
// Use this for initialization
void Start () {
}
void Update(){
if(Input.GetMouseButtonDown(0)){
hit_position = Input.mousePosition;
camera_position = transform.position;
}
if(Input.GetMouseButton(0)){
current_position = Input.mousePosition;
LeftMouseDrag();
}
}
void LeftMouseDrag(){
// From the Unity3D docs: "The z position is in world units from the camera." In my case I'm using the y-axis as height
// with my camera facing back down the y-axis. You can ignore this when the camera is orthograhic.
current_position.z = hit_position.z = camera_position.y;
// Get direction of movement. (Note: Don't normalize, the magnitude of change is going to be Vector3.Distance(current_position-hit_position)
// anyways.
Vector3 direction = Camera.main.ScreenToWorldPoint(current_position) - Camera.main.ScreenToWorldPoint(hit_position);
// Invert direction to that terrain appears to move with the mouse.
direction = direction * -1;
Vector3 position = camera_position + direction;
transform.position = position;
}
}
I've come up with this script (I have appended it to the camera):
private Vector2 worldStartPoint;
void Update () {
// only work with one touch
if (Input.touchCount == 1) {
Touch currentTouch = Input.GetTouch(0);
if (currentTouch.phase == TouchPhase.Began) {
this.worldStartPoint = this.getWorldPoint(currentTouch.position);
}
if (currentTouch.phase == TouchPhase.Moved) {
Vector2 worldDelta = this.getWorldPoint(currentTouch.position) - this.worldStartPoint;
Camera.main.transform.Translate(
-worldDelta.x,
-worldDelta.y,
0
);
}
}
}
// convert screen point to world point
private Vector2 getWorldPoint (Vector2 screenPoint) {
RaycastHit hit;
Physics.Raycast(Camera.main.ScreenPointToRay(screenPoint), out hit);
return hit.point;
}
Pavel's answer helped me a lot, so wanted to share my solution with the community in case it helps others. My scenario is a 3D world with an orthographic camera. A top-down style RTS I am working on. I want pan and zoom to work like Google Maps, where the mouse always stays at the same spot on the map when you pan and zoom. This script achieves this for me, and hopefully is robust enough to work for others' needs. I haven't tested it a ton, but I commented the heck out of it for beginners to learn from.
using UnityEngine;
// I usually attach this to my main camera, but in theory you can attach it to any object in scene, since it uses Camera.main instead of "this".
public class CameraMovement : MonoBehaviour
{
private Vector3 MouseDownPosition;
void Update()
{
// If mouse wheel scrolled vertically, apply zoom...
// TODO: Add pinch to zoom support (touch input)
if (Input.mouseScrollDelta.y != 0)
{
// Save location of mouse prior to zoom
var preZoomPosition = getWorldPoint(Input.mousePosition);
// Apply zoom (might want to multiply Input.mouseScrollDelta.y by some speed factor if you want faster/slower zooming
Camera.main.orthographicSize = Mathf.Clamp(Camera.main.orthographicSize + Input.mouseScrollDelta.y, 5, 80);
// How much did mouse move when we zoomed?
var delta = getWorldPoint(Input.mousePosition) - preZoomPosition;
// Rotate camera to top-down (right angle = 90) before applying adjustment (otherwise we get "slide" in direction of camera angle).
// TODO: If we allow camera to rotate on other axis we probably need to adjust that also. At any rate, you want camera pointing "straight down" for this part to work.
var rot = Camera.main.transform.localEulerAngles;
Camera.main.transform.localEulerAngles = new Vector3(90, rot.y, rot.z);
// Move the camera by the amount mouse moved, so that mouse is back in same position now.
Camera.main.transform.Translate(delta.x, delta.z, 0);
// Restore camera rotation
Camera.main.transform.localEulerAngles = rot;
}
// When mouse is first pressed, just save location of mouse/finger.
if (Input.GetMouseButtonDown(0))
{
MouseDownPosition = getWorldPoint(Input.mousePosition);
}
// While mouse button/finger is down...
if (Input.GetMouseButton(0))
{
// Total distance finger/mouse has moved while button is down
var delta = getWorldPoint(Input.mousePosition) - MouseDownPosition;
// Adjust camera by distance moved, so mouse/finger stays at exact location (in world, since we are using getWorldPoint for everything).
Camera.main.transform.Translate(delta.x, delta.z, 0);
}
}
// This works by casting a ray. For this to work well, this ray should always hit your "ground". Setup ignore layers if you need to ignore other colliders.
// Only tested this with a simple box collider as ground (just one flat ground).
private Vector3 getWorldPoint(Vector2 screenPoint)
{
RaycastHit hit;
Physics.Raycast(Camera.main.ScreenPointToRay(screenPoint), out hit);
return hit.point;
}
}
Based on the answer from Pavel, I simplified the script and removed the unlovely "jump" when touch with more then one finger and release the second finger:
private bool moreThenOneTouch = false;
private Vector3 worldStartPoint;
void Update() {
Touch currentTouch;
// only work with one touch
if (Input.touchCount == 1 && !moreThenOneTouch) {
currentTouch = Input.GetTouch(0);
if (currentTouch.phase == TouchPhase.Began) {
this.worldStartPoint = Camera.main.ScreenToWorldPoint(currentTouch.position);
}
if (currentTouch.phase == TouchPhase.Moved) {
Vector3 worldDelta = Camera.main.ScreenToWorldPoint(currentTouch.position) - this.worldStartPoint;
Camera.main.transform.Translate(
-worldDelta.x,
-worldDelta.y,
0
);
}
}
if (Input.touchCount > 1) {
moreThenOneTouch = true;
} else {
moreThenOneTouch = false;
if(Input.touchCount == 1)
this.worldStartPoint = Camera.main.ScreenToWorldPoint(Input.GetTouch(0).position);
}
}
using UnityEngine;
// I usually attach this to my main camera, but in theory you can attach it to any object in scene, since it uses Camera.main instead of "this".
public class CameraMovement : MonoBehaviour
{
private Vector3 MouseDownPosition;
void Update()
{
// If mouse wheel scrolled vertically, apply zoom...
// TODO: Add pinch to zoom support (touch input)
if (Input.mouseScrollDelta.y != 0)
{
// Save location of mouse prior to zoom
var preZoomPosition = getWorldPoint(Input.mousePosition);
// Apply zoom (might want to multiply Input.mouseScrollDelta.y by some speed factor if you want faster/slower zooming
Camera.main.orthographicSize = Mathf.Clamp(Camera.main.orthographicSize + Input.mouseScrollDelta.y, 5, 80);
// How much did mouse move when we zoomed?
var delta = getWorldPoint(Input.mousePosition) - preZoomPosition;
// Rotate camera to top-down (right angle = 90) before applying adjustment (otherwise we get "slide" in direction of camera angle).
// TODO: If we allow camera to rotate on other axis we probably need to adjust that also. At any rate, you want camera pointing "straight down" for this part to work.
var rot = Camera.main.transform.localEulerAngles;
Camera.main.transform.localEulerAngles = new Vector3(90, rot.y, rot.z);
// Move the camera by the amount mouse moved, so that mouse is back in same position now.
Camera.main.transform.Translate(delta.x, delta.z, 0);
// Restore camera rotation
Camera.main.transform.localEulerAngles = rot;
}
// When mouse is first pressed, just save location of mouse/finger.
if (Input.GetMouseButtonDown(0))
{
MouseDownPosition = getWorldPoint(Input.mousePosition);
}
// While mouse button/finger is down...
if (Input.GetMouseButton(0))
{
// Total distance finger/mouse has moved while button is down
var delta = getWorldPoint(Input.mousePosition) - MouseDownPosition;
// Adjust camera by distance moved, so mouse/finger stays at exact location (in world, since we are using getWorldPoint for everything).
Camera.main.transform.Translate(delta.x, delta.z, 0);
}
}
// This works by casting a ray. For this to work well, this ray should always hit your "ground". Setup ignore layers if you need to ignore other colliders.
// Only tested this with a simple box collider as ground (just one flat ground).
private Vector3 getWorldPoint(Vector2 screenPoint)
{
RaycastHit hit;
Physics.Raycast(Camera.main.ScreenPointToRay(screenPoint), out hit);
return hit.point;
}
}