Detecting a click on a specific tile in a tilemap - c#

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)
{
...
}
}

Related

Why Unity hasTile display False?

Im created a 3d tilemap using free asset models as shown below:
and here is the code where I am trying to get its position:
// Update is called once per frame
void Update()
{
if(Input.GetMouseButtonDown(0))
{
//read in another post that changing to vector 2 will help
Vector2 mousePosition = (Vector2)Input.mousePosition;
Ray ray = cameraMain.ScreenPointToRay(mousePosition);
//performing raycast
if(Physics.Raycast(ray, out RaycastHit hitInfo))
{
//checking if hit the collider of gameobject and checking if the renderer is not null
if(hitInfo.collider.gameObject.GetComponent<Target>() != null)
{
Vector3 rayPosition = hitInfo.point;
//Vector3Int posInt = grid.WorldToCell(hitInfo.point);
Vector3Int posInt = grid.LocalToCell(rayPosition);
//Vector3Int posInt2 = tilemap.WorldToCell(mp);
//Shows the cell reference for the grid
Debug.Log(posInt);
// Shows the name of the tile at the specified coordinates
Debug.Log(tilemap.HasTile(posInt));
//Debug.Log(tilemap.GetTile(posInt).name);
}
}
}
}
and this is the result:
I want to use getTile, but it keeps saying object reference not set to the instance of the object, so I used hasTile to check if the tile is there. For some reason it is displaying false. Can someone tell me what is wrong?

2D Raycast does not correctly detect overlapping colliders and ignores Sorting Layer

I want to be able to interact with my gameobjects while organising my sprites with Sorting Layer.
Right now, Sorting Layer do not affect my Raycast/collider situation and only change the visual appearance of my order.
In my Test scene I have two sprites: a Background and one Object.
Both have a 2D Box Collider component.
My Raycast script is suppose to detect on which Object my mouse is hovering on; either the Background (BG) or the Object, which lays on top of the BG.
As long as the BG collider is enabled in the inspector, my script will ONLY recognize the BG, even if the other object lays on top of my BG (visually and according to the Sorting Layer) (both Z-positions = 0, Sorting Layer Default (BG: 0, Object 1)).
The collider have the correct size and if I disable or remove the Collider from the BG, my script will detect the other Object just fine.
The only way to make sure that my Object gets detected, while my BG collider is activated is by changing the Objects Z position to a position between my BG and my Camera (between smaller 0 and greater -10)
I tried to:
this is a fresh 2D project, so my cameras Projection is set to Orthograph, my BG and Object use Sprite Renderer and is use 2D collider
change the "Order in Layer", back and forth, including negative numbers.
add a Foreground (and Background) "Sorting Layer"
(both options only had a visual effect and did not affect the Raycast output)
change the positions in the hierarchy.
removing and re-adding the colliders. (!) I observed that the 2D Box collider I added last, will always be behind every other colliders. This is something I can work with for now. But will become a problem later in the future, when the project gets bigger.
I want to be able to interact with items and doors etc. (clicks and reactions to mouse hovering) in my 2D click adventure. If 2D Raycast is not the way to go to archive this, I would be glad to know about the proper technique to use in this situation.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RayCast : MonoBehaviour
{
[SerializeField] private Vector2 mousePosWorld2D;
RaycastHit2D hit;
void Update()
{
mousePosWorld2D = (Vector2)Camera.main.ScreenToWorldPoint(Input.mousePosition);
hit = Physics2D.Raycast(mousePosWorld2D, Vector2.zero);
if (hit.collider)
{
Debug.Log("Found collider: " + hit.collider.name);
}
else
{
Debug.Log("No collider found.");
}
}
}
I expected the "Sorting Layer" or "Order in Layer" to have an impact on my Raycast, while changing the Z position does nothing. But it is the other way around. Sorting Layer only have a visual effect and moving my Object closer to the camera (Z position) helps to detect the right collider.
How do I solve the problem?
What gets hit first from a Ray has only to do with how close the collider is to the camera, and nothing with the order in the Hierarchy or the SortinLayers.
To find the "highest" RaycastTarget based on the SortingLayer, I created the Script below.
We fire a Raycast and iterate over all the Colliders that we hit, by using a foreach-loop. Then the function will return the GameObject with the highest SortingLayer. Note: If there is more than one GameObject with the highest SortingLayer, the function will return the last one it found.
using UnityEngine;
public class Raycast : MonoBehaviour
{
// The name of the relevant Layer
[SerializeField] string layerToCheck = "Default";
void Update()
{
Vector2 mousePosWorld2D = (Vector2)Camera.main.ScreenToWorldPoint(Input.mousePosition);
GameObject result = GetHighestRaycastTarget(mousePosWorld2D);
// Do Stuff example:
Debug.Log($"Highest Layer: {result}");
if (Input.GetMouseButtonDown(0)
&& result != null)
{
Destroy(result);
}
}
// Get highest RaycastTarget based on the Sortinglayer
// Note: If multiple Objects have the same SortingLayer (e.g. 42) and this is also the highest SortingLayer, then the Function will return the last one it found
private GameObject GetHighestRaycastTarget(Vector2 mousePos)
{
GameObject topLayer = null;
RaycastHit2D[] hit = Physics2D.RaycastAll(mousePos, Vector2.zero);
foreach (RaycastHit2D ray in hit)
{
SpriteRenderer spriteRenderer = ray.transform.GetComponent<SpriteRenderer>();
// Check if RaycastTarget has a SpriteRenderer and
// Check if the found SpriteRenderer uses the relevant SortingLayer
if (spriteRenderer != null
&& spriteRenderer.sortingLayerName == layerToCheck)
{
if (topLayer == null)
{
topLayer = spriteRenderer.transform.gameObject;
}
if (spriteRenderer.sortingOrder >= topLayer.GetComponent<SpriteRenderer>().sortingOrder)
{
topLayer = ray.transform.gameObject;
}
}
}
return topLayer;
}
}
Have you tried with the script that it's in the Unity official documentation?
https://docs.unity3d.com/ScriptReference/Physics.Raycast.html
using UnityEngine;
// C# example.
public class ExampleClass : MonoBehaviour
{
void Update()
{
// Bit shift the index of the layer (8) to get a bit mask
int layerMask = 1 << 8;
// This would cast rays only against colliders in layer 8.
// But instead we want to collide against everything except layer 8. The ~ operator does this, it inverts a bitmask.
layerMask = ~layerMask;
RaycastHit hit;
// Does the ray intersect any objects excluding the player layer
if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), out hit, Mathf.Infinity, layerMask))
{
Debug.DrawRay(transform.position, transform.TransformDirection(Vector3.forward) * hit.distance, Color.yellow);
Debug.Log("Did Hit");
}
else
{
Debug.DrawRay(transform.position, transform.TransformDirection(Vector3.forward) * 1000, Color.white);
Debug.Log("Did not Hit");
}
}
}

Mouse events for Unity3D isometric tile map

I have been reading up on the new tile map system in Unity3D. I have managed to get to the point of setting up a grid -> tile-map and setting up a tile palette. However now i'm struggling with finding up-to-date tutorials for handling mouse events for this new tile map system.
I'm attempting to set a highlight when the mouse is over the tile and if the tile is clicked I want to be able to trigger scripts and other events. However the available tutorials online don't go into mouse events for the tile map system and very few talk about isometric tile maps.
Are there any good up to date tutorials for handling mouse events on an isometric tile map? Even a simple tutorial showing a hover effect on the tile and a "hello world from tile x.y" when tile is clicked, would be all i would really need to get going.
This is what I have so far:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MouseManager : MonoBehaviour
{
void Update()
{
Vector3 clickPosition = Vector3.one;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if(Physics.Raycast(ray, out hit))
{
clickPosition = hit.point;
}
Debug.Log(clickPosition);
}
}
This should get you started:
using UnityEngine;
using UnityEngine.Tilemaps;
public class Test : MonoBehaviour {
//You need references to to the Grid and the Tilemap
Tilemap tm;
Grid gd;
void Start() {
//This is probably not the best way to get references to
//these objects but it works for this example
gd = FindObjectOfType<Grid>();
tm = FindObjectOfType<Tilemap>();
}
void Update() {
if (Input.GetMouseButtonDown(0)) {
Vector3 pos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
Vector3Int posInt = gd.LocalToCell(pos);
//Shows the cell reference for the grid
Debug.Log(posInt);
// Shows the name of the tile at the specified coordinates
Debug.Log(tm.GetTile(posInt).name);
}
}
}
In short, get a reference to the grid and tilemap. Find the local coordinates using ScreenToWorldPoint(Input.mousePosition). Call the LocalToCell method of the grid object to get your local coordinates (Vector3) converted to cell coordinates (Vector3Int). Pass the cell coordinates to the GetTile method of the Tilemap object to get the Tile (then use the methods associated with the Tile class to make whatever changes you want to make).
In this example, I just attached the above script to an empty GameObject in the world. It would probably make sense to attach it to the Grid, instead. The general logic remains the same nonetheless.
This is a slightly different version from the way HumanWrites does it. It doesn't require a reference to the grid, and the mousePos is declared as a Vector2, rather than a Vector3 - this will avoid problems when working in 2D.
using UnityEngine;
using UnityEngine.Tilemaps;
public class MouseManager : MonoBehaviour
{
private Tilemap tilemap;
void Start()
{
tilemap = FindObjectOfType<Tilemap>();
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Vector2 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
Vector3Int gridPos = tilemap.WorldToCell(mousePos);
if (tilemap.HasTile(gridPos))
Debug.Log("Hello World from " + gridPos);
}
}
}
The 'tilemap' that we're referencing is a gameObject in your scene. You may have renamed it to something else, but it would be a child of the 'Grid' object.

OnMouseDown() called for only the foremost GameObject

(source: ibin.co)
I have a BoxCollider2D component and a script attached to the black and red boxes. Each script with some things to happen in a OnMouseDown() function. The problem is if I click on the part of the orange box that overlaps the black one, the OnMouseDown() of the black box will be called but I want only the orange box function to be called.
How do you achieve this?
I don't think OnMouseDown is a great approach here. You can use 2D RayCast to only get the top most collider. You are going to have to account for sorting layer and sorting order on your own with this approach.
The trick to checking a single point in 2D is to not give the raycast direction as shown below with:
Physics2D.Raycast(touchPostion, Vector2.zero);
Here is an example I threw together that doesn't account for using sorting layers, just sorting order.
using UnityEngine;
public class RayCastMultiple : MonoBehaviour
{
//Current active camera
public Camera MainCamera;
// Use this for initialization
void Start ()
{
if(MainCamera == null)
Debug.LogError("No MainCamera");
}
// Update is called once per frame
void FixedUpdate ()
{
if(Input.GetMouseButtonDown(0))
{
var mouseSelection = CheckForObjectUnderMouse();
if(mouseSelection == null)
Debug.Log("nothing selected by mouse");
else
Debug.Log(mouseSelection.gameObject);
}
}
private GameObject CheckForObjectUnderMouse()
{
Vector2 touchPostion = MainCamera.ScreenToWorldPoint(Input.mousePosition);
RaycastHit2D[] allCollidersAtTouchPosition = Physics2D.RaycastAll(touchPostion, Vector2.zero);
SpriteRenderer closest = null; //Cache closest sprite reneder so we can assess sorting order
foreach(RaycastHit2D hit in allCollidersAtTouchPosition)
{
if(closest == null) // if there is no closest assigned, this must be the closest
{
closest = hit.collider.gameObject.GetComponent<SpriteRenderer>();
continue;
}
var hitSprite = hit.collider.gameObject.GetComponent<SpriteRenderer>();
if(hitSprite == null)
continue; //If the object has no sprite go on to the next hitobject
if(hitSprite.sortingOrder > closest.sortingOrder)
closest = hitSprite;
}
return closest != null ? closest.gameObject : null;
}
}
This is pretty much just a duplicate of my answer here: Game Dev Question

Unity C# camera zoom script

So before I explain my problem. I will first tell what I am really doing.
I am working on a click to move/zoom camera script. There are 3 planes in front of my Main Camera. Now what I am doing is, creating a script which says " The camera will zoom on the plane which gets clicked. I made several attempts to come up with a working script but it didn't worked well. Every time I come across new bugs , errors and what not. :|
I got frustrated and deleted the buggy script. Now I want to start from scratch. I am doing it in C#
Since I am not a professional, Can anyone explain me in detail to get it done?
I am confused how to deal with the planes I placed. I want to know what is missing in my script.
Here is a screenshot of how I placed those planes.
Edit. - I managed to work on it. Now I need advice, how to target the planes I placed in front of the camera.
using UnityEngine;
using System.Collections;
public class CameraZoom : MonoBehaviour
{
public int zoomLevelSelected = 0;
public float[] ZoomLevels = new float[] { 60, 40, 20 };
void Update()
{
int zoomChange = 0;
if (Input.GetMouseButtonDown(0)) { zoomChange = +1; } // back
else if (Input.GetMouseButtonDown(1)) { zoomChange = -1; } // forward
if (zoomChange != 0)
{
zoomLevelSelected = Mathf.Clamp(zoomLevelSelected + zoomChange, 0, ZoomLevels.Length - 1);
camera.fieldOfView = ZoomLevels[zoomLevelSelected];
}
}
}
Heck with it, here is one way to create a click zoom. The gist is that you create a ray from your camera in to the scene through the mouse cursor. When that ray intersects an object create a second ray from the point of intersection back out along the intersecting face's normal.
void Update () {
if(Input.GetMouseButtonDown(0)){
// get ray from camera in to scene at the mouse position
Ray ray = Camera.mainCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
// hardcoded "zoom" distance.
float zoomDist = 15.0f;
// Raycast from camera to mouse cursor, if object hit, zoom.
if (Physics.Raycast(ray,out hit,Mathf.Infinity)){
// Create a second ray from the hit object back out, zoom the camera along this ray.
Ray r = new Ray(hit.point,hit.normal);
Camera.mainCamera.transform.position = r.GetPoint(zoomDist);
}
}
}
Things to keep in mind:
Physics.Raycast, as written, will return true for any GameObject with a collider. Use layers if you only want to zoom when selecting specific GameObjects.
The camera won't directly center on the GameObject you click. I use the exact point of intersection to create the position where the camera will zoom to.
zoomDist is the distance away from the object.
This code only works with perspective cameras, if you use orthographic you'll need to modify the size value of the camera to zoom.
The problem with your script is that your var int zoomChange is getting set to zero every frame so move that variable to class level.
using UnityEngine;
using System.Collections;
public class CameraZoom : MonoBehaviour
{
public int zoomLevelSelected = 0;
public float[] ZoomLevels = new float[] { 60, 40, 20 };
int zoomChange = 0; //<<<<<<<<<<<<<
void Update()
{
if (Input.GetMouseButtonDown(0)) { zoomChange = +1; } // back
else if (Input.GetMouseButtonDown(1)) { zoomChange = -1; } // forward
if (zoomChange != 0)
{
zoomLevelSelected = Mathf.Clamp(zoomLevelSelected + zoomChange, 0, ZoomLevels.Length - 1);
camera.fieldOfView = ZoomLevels[zoomLevelSelected];
}
}
}

Categories