How to change the position of GameObjects with Unity Input.GetMouseButtonDown method? - c#

I already know how to click on 3D Objects in the scene by using the Input.GetMouseButtonDown. I'm trying to change the 3D Object position by clicking on the object. I added a Box Collider in the object and I'm calling the following method.
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
foreach (GameObject child in floorTiles) {
BoxCollider box = child.GetComponentInChildren<BoxCollider>();
if (hit.transform.name.Equals(box.name))
{
handleOnMouseDown(hit.collider);
}
}
}
}
}
floorTiles is an array of GameObjects.
If I hit one of these objects the below function is called:
void handleOnMouseDown(Collider box)
{
GameObject parent = box.transform.parent.gameObject;
Vector3 position = parent.transform.position;
positionX.GetComponent<TextMeshProUGUI>().text = position.x.ToString();
posXButtonPlus.GetComponent<Button>().onClick.AddListener(() => handleOnChangePosition("posx", parent));
}
This works, however, when I click many objects, all the last objects clicked also keep changing their positions. How can I change one position per time?

Each click on an object, adds listener to your button, but you don't ever remove listeners. You end up with multiple listeners, that's why more objects are moved than intended.
You could remove listener after each button click, but that seems like a total overkill.
Instead of adding multiple listeners, consider adding just one which will remember the last clicked object, and clicking your button will move only that object.
Also, if you want to move objects just by clicking on them, not on button click, you can simplify all these, and move the object directly in handleOnMouseDown.
Remove listeners variant:
void handleOnMouseDown(Collider box)
{
posXButtonPlus.GetComponent<Button>().onClick.AddListener(() => handleOnChangePosition("posx", box.gameObject));
}
void handleOnChangePosition(string pos, GameObject go)
{
// I don't have code for moving, but I imagine looks something like this. right * 0.5f can be anything, did it just for tests.
go.transform.position += Vector3.right * 0.5f;
posXButtonPlus.GetComponent<Button>().onClick.RemoveAllListeners();
}
Last clicked variant:
GameObject lastClicked;
void Awake()
{
posXButtonPlus.GetComponent<Button>().onClick.AddListener(() => handleOnChangePosition());
}
void handleOnMouseDown(Collider box)
{
lastClicked = box.gameObject;
}
void handleOnChangePosition()
{
lastClicked.transform.position += Vector3.right * 0.5f;
}
Without buttons and other stuff:
void handleOnMouseDown(Collider box)
{
box.transform.position += Vector3.right * 0.5f;
}

Related

How to detect when a cube is directly above another cube?

I am using the Unity Engine with C#.
I have a 1x1 cube which moves forward on a grid of 49, 1x1 cubes (screenshot below) - when I press the start button on the controller.
The movement code for the cube is below.
void MovePlayerCube()
{
transform.Translate(direction * moveSpeed * Time.deltaTime);
}
When this cube passes over a cube with an arrow on it, the cube will change direction to where the arrow is pointing (staying on the same Y axis).
I need to detect the exact point at which the cube is directly over the cube with the arrow on it, and run the 'change direction' code at that point.
I'm currently using Vector3.Distance to check if the X and Z coordinates of the 2 cubes are close enough together (if they are less than 0.03f in distance), I can't check if they are equal due to floating point imprecision.
However this is really ineffective as half the time this code doesn't register for probably the same reason, and if I increase the 0.03f to a point where it never misses it becomes really noticeable that the cube isn't aligned with the grid anymore.
There has to be a proper solution to this and hopefully I've clarified the situation enough?
Any advice is appreciated.
You are moving your cube via
transform.Translate(direction * moveSpeed * Time.deltaTime);
which will never be exact an might overshoot your positions.
=> I would rather implement a coroutine for moving the cube exactly one field at a time, ensuring that after each iteration it fully aligns with the grid and run your checks once in that moment.
It doesn't even have to match exactly then, you only need to check if you are somewhere hitting a cube below you.
So something like e.g.
private Vector3Int direction = Vector3Int.left;
private IEnumerator MoveRoutine()
{
// depends on your needs if this runs just forever or certain steps
// or has some exit condition
while(true)
{
// calculate the next position
// optional round it to int => 1x1 grid ensured on arrival
// again depends a bit on your needs
var nextPosition = Vector3Int.RoundToInt(transform.position) + direction;
// move until reaching the target position
// Vector3 == Vector3 uses a precision of 1e-5
while(transform.position != nextPosition)
{
transform.position = Vector3.MoveTowards(transform.position, nextPosition, moveSpeed * Time.deltaTime);
yield return null;
}
// set target position in one frame just to be sure
transform.position = nextPosition;
// run your check here ONCE and adjust direction
}
}
start this routine only ONCE via
StartCoroutine(MoveRoutine());
or if you have certain exit conditions at least only run one routine at a time.
A Corouine is basically just a temporary Update routine with a little bit different writing => of course you could implement the same in Update as well if you prefer that
private Vector3Int direction = Vector3Int.left;
private Vector3 nextPosition;
private void Start()
{
nextPosition = transform.position;
}
private void Update()
{
if(transform.position != nextPosition)
{
transform.position = Vector3.MoveTowards(transform.position, nextPosition, moveSpeed * Time.deltaTime);
}
else
{
transform.position = nextPosition;
// run your check here ONCE and adjust direction
// then set next position
nextPosition = Vector3Int.RoundToInt(transform.position) + direction;
}
}
Then regarding the check you can have a simple raycast since you only run it in a specific moment:
if(Physics.Raycast(transform.position, Vector3.down, out var hit))
{
direction = Vector3Int.RountToInt(hit.transform.forward);
}
assuming of course your targets have colliders attached, your moved cube position (pivot) is above those colliders (assumed it from your image) and your targets forward actually points int the desired new diretcion
I would do it this way. First I would split the ability of certain objects to be "moving with certain speed" and "moving in a certain direction", this can be done with C# interfaces. Why? Because then your "arrow" cube could affect not only your current moving cube, but anything that implements the interfaces (maybe in the future you'll have some enemy cube, and it will also be affected by the arrow modifier).
IMovingSpeed.cs
public interface IMovementSpeed
{
float MovementSpeed{ get; set; }
}
IMovementDirection3D.cs
public interface IMovementDirection3D
{
Vector3 MovementDirection { get; set; }
}
Then you implement the logic of your cube that moves automatically in a certain direction. Put this component on your player cube.
public class MovingStraight: MonoBehaviour, IMovementSpeed, IMovementDirection3D
{
private float _movementSpeed;
Vector3 MovementSpeed
{
get { return _movementSpeed; }
set { _movementSpeed = value; }
}
private Vector3 _movementDirection;
Vector3 MovementDirection
{
get { return _movementDirection; }
set { _movementDirection= value; }
}
void Update()
{
// use MovementSpeed and MovementDirection to advance the object position
}
}
Now to implement how the arrow cube modifies other objects, I would attach a collision (trigger) component for both moving cube and the arrow cube.
In the component of the arrow cube, you can implement an action when something enters this trigger zone, in our case we check if this object "has direction that we can change", and if so, we change the direction, and make sure that the object will be aligned, by forcing the arrow cube's position and the other object's position to be the same on the grid.
public class DirectionModifier: MonoBehaviour
{
private Vector3 _newDirection;
private void OnTriggerEnter(Collider collider)
{
IMovementDirection3D objectWithDirection = collider as IMovementDirection3D ;
if (objectWithDirection !=null)
{
objectWithDirection.MovementDirection = _newDirection;
// to make sure the object will continue moving exactly
// from where the arrow cube is
collider.transform.position.x = transform.position.x;
collider.transform.position.y = transform.position.y;
}
}
}
If you made your trigger zones too large, however, then the moving cube will "jump" abruptly when it enters the arrow cube's trigger zone. You can fix it by either starting a coroutine as other answers suggested, or you could make the trigger zones pretty small, so that the jump is not noticeable (just make sure not to make them too small, or they may not intersect each other)
You could then similarly create many other modifying blocks, that would change speed or something
I think that it is enough for you to check if the X and Z coordinates are equal, since the movement occurs only along them
Example
if(_player.transfom.position.x == _gameSquare.transfom.position.x && _player.transfom.position.z == _gameSquare.transfom.position.z)
{
Debag.Log("Rotate!")
}

Rotation of a single object when script is attached to multiple objects

I have a game in Unity in which I have added three objects (Cube, Cylinder, Capsule). To these game objects, I have added a script to rotate them along an axis when they press the left click and x, y or z, rotating the object along the respective axis. But when I attempt rotate a single object, the other objects rotate too. How do I rotate each object without affecting the other objects.
if (Input.GetMouseButton(0) && Input.GetKey(KeyCode.Z))
{
transform.RotateAround(transform.position, transform.forward, Time.deltaTime * 90f);
}
if (Input.GetMouseButton(0) && Input.GetKey(KeyCode.X))
{
transform.RotateAround(transform.position, transform.right, Time.deltaTime * 90f);
}
if (Input.GetMouseButton(0) && Input.GetKey(KeyCode.Y))
{
transform.RotateAround(transform.position, transform.up, Time.deltaTime * 90f);
}
I assume what you are trying to achieve is
click on an object with the mouse Cursor and hold
then rotate this object using the according keys.
You actually need only one single script for this, not on every object but just one in your scene (like a central controller)
public class ObjectRotator : MonoBehaviour
{
[SerializeField] private Camera _camera;
//Optionally if you only want to hit objects on (a) certain layer(s)
//[SerializeField] private LayerMask layers;
private void Awake ()
{
if(!_camera) _camera = Camera.main;
}
private void Update ()
{
if(Input.GetMouseButton(0))
{
// Shoot a Raycast from the mouse position into your scene to check if you hit an object
var ray = _camera.ScreenPointToRay(Input.mousePosition);
if(Physics.Raycast(ray, out var hit)
// or if using the layer mask
//if(Physics.Raycast(ray, out var hit, float.positiveInfinity, layers)
{
if (Input.GetKey(KeyCode.Z))
{
// Instead of using RotateAround rather use Rotate which rotated around this object's
// pivot position anyway. By default it is in local space so also no need for transform.forward etc
hit.transform.Rotate(Vector3.forward * Time.deltaTime * 90f);
}
if (Input.GetKey(KeyCode.X))
{
hit.transform.Rotate(Vector3.right * Time.deltaTime * 90f);
}
if (Input.GetKey(KeyCode.Y))
{
hit.transform.Rotate(Vector3.up * Time.deltaTime * 90f);
}
}
}
}
}
NOTE: Requires your target objects to have colliders (and the selected layer(s))
As an alternative if you rather want to go for a solution with one script per object you could rather use e.g.
public class Rotation : MonoBehaviour
{
// This is called by Unity when the mouse is going down while hovering this object's collider
private void OnMouseDown ()
{
// Start the RotationRoutine
StartCoroutine (RotationRoutine());
}
// This is called when the mouse button goes up while hovering this object's collider
private void OnMouseUp()
{
// To simplify things just stop any routine started by this behavior
StopAllCoroutines ();
}
// Also stop when the mouse leaves this object's collider while still being pressed
private void OnMouseExit()
{
StopAllCoroutines ();
}
private void RotationRoutine()
{
// Whut?! No worries ;) This is fine in a Coroutine as soon as you "yield" somewhere inside
while(true)
{
if (Input.GetKey(KeyCode.Z))
{
transform.Rotate(Vector3.forward * Time.deltaTime * 90f);
}
if (Input.GetKey(KeyCode.X))
{
transform.Rotate(Vector3.right * Time.deltaTime * 90f);
}
if (Input.GetKey(KeyCode.Y))
{
transform.Rotate(Vector3.up * Time.deltaTime * 90f);
}
// Basically tells Unity to "pause" here, render this frame and
// continue from here in the next frame
yield return null;
}
}
}
Try three separate monobehaviour with input condition Input.GetKey(KeyCode.X), Input.GetKey(KeyCode.Y), Input.GetKey(KeyCode.Z) respectively:
if (Input.GetMouseButton(0) && Input.GetKey(KeyCode.X)) {
transform.RotateAround(transform.position, transform.forward, Time.deltaTime * 90f);
}
The transform applies to the transform of the gameObject the Monobehvaiour is attached to, so if the you have the script attached to many gameobjects, the logic applies to all of them.
You need to make 3 different Monobehaviours and handle the input logic respectively, or make a global Monobehaviour rotation handler, where you keep a reference for each of your gameObjects so that you can handle each of the transforms respectively.

How to check if clicked on UI object?

I am new to Unity and trying to make a basic minesweeper game. I have a square prefab and I want to learn if it is clicked. But I can not do it listening because I need both left click and right click. How can I do that?
The absolute quickest way should be to implement something like this on the prefab you wish to listen for clicks on:
void OnMouseOver()
{
if (Input.GetMouseDown(0)) {
// Left click
}
else if (Input.GetMouseDown(1)) {
// Right click
}
}
If you instead want to detect mouseclicks from a more central position (as in, not distributed to each GameObject) you will need to create a component that fires Raycasts depending on left & right-clicks and look for specific objects, and then do you logic
Short example:
if (Input.GetMouseDown(0)) {
if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out hit, 100)) {
// something was hit
}
}
https://docs.unity3d.com/ScriptReference/Physics.Raycast.html
You can try usign the Input.GetKeyDown function in the Update:
void Update()
{
if (Input.GetKeyDown(KeyCode.Mouse0))
{
//print("Right click");
}
else if (Input.GetKeyDown(KeyCode.Mouse1))
{
//print("Left click");
}
}
For detecting if the mouse is over the spesific game object you can use:
void OnMouseOver()
{
///
}
Or
Ray ray;
RaycastHit hit;
void Update()
{
ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if(Physics.Raycast(ray, out hit))
{
print (hit.collider.name);
}
}

Issues with camera reset using a script in Unity

I want to centre the camera on the object and then, if player clicks on button (which will be added later) or outside of the pop-up which will be triggered by clicking on object (camera movement is just a first step).
My idea was to store the very first position of the camera and transform it back again if mouse is clicking outside of any clickable objects, but looks like it's not working.
void Update()
{
Vector3 camoriginposition;
if (Input.GetMouseButtonDown(0))
{
Camera cam = Camera.main;
camoriginposition = Camera.main.transform.position;
RaycastHit hit;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit))
{
Rigidbody rb;
if (rb = hit.transform.GetComponent<Rigidbody>())
{
Vector3 obj = rb.transform.position - new Vector3(0, -3, 2);
cam.transform.position = obj;
Debug.Log("curr cam pos" + cam.transform.position);
}
else
{
cam.transform.position = camoriginposition;
Debug.Log("It triggers!");
}
}
}
}
I receive the Debug.Log output, but looks like transform.position cannot be processed.
Do you have any ideas how to fix it?
I'm not sure what behaviour you are meaning by "not working", but maybe It's because you are declaring camoriginposition on every Update(), which would be definitely not the desired behaviour. Whenever you tries to store your cam's origin position to local variable, it will be gone when the Update() scope ends, and will be initialized again with new Update() call.
You have to seperate camoriginposition from Update(), like:
private Vector3 camoriginposition;
void Update()
{
if (Input.GetMouseButtonDown(0))
{
...
}
And, it seems like there are another problem. You are storing camoriginposition on every mouse click, but with your description, It seems like it should be stored when the ray actually hits the gameobject.
You should move camoriginposition = Camera.main.transform.position;
into
if (rb = hit.transform.GetComponent<Rigidbody>()) block.

dragging/dropping and triggers

I am trying to create a drag and drop functionality. I have a game object that lies on a cube (like a table). I want to allow the player to drag and drop this object to multiple "hot spots". At the same time, while this is a 3D game, they should not be allowed to drag the object off of the table, above it, bellow it, etc. Just drag across the top of the table. The way I have this setup is like this:
On the table top (a cube with a rigid body)I have two planes. This plane is just decorative to let the user see where to drop to. Each of these planes has a child plane (a much smaller area, centered in the parent). This child has a Box Collider (isTrigger= true) that extends high up into the air, like a pole sticking out of the ground)
I then have a cube that has a rigid body and a box collider (isTrigger= true). This cube is instantiated onto the "table" top, over one of the hot spots.
HotSpotCode.cs
public class HotSpotCodeScript : MonoBehaviour {
public void OnTriggerEnter(Collider other){
DraggableObject ds = other.GetComponent<DraggableObject>();
ds.startPos = this.gameObject.transform.position;
}
}
DraggableObject.cs
public class DraggableObject : MonoBehaviour {
float lerpTime = 1f;
float currentLerpTime;
float moveDistance = 10f;
Plane plane;
public Vector3 startPos, endPos;
public bool drag;
Vector3 position;
void Start(){
startPos = transform.position;
endPos = transform.position + transform.up * moveDistance;
}
void OnMouseDown(){
plane = new Plane(Vector2.up, transform.position);
drag = true; // start dragging
}
void OnMouseUp(){
lerp();
drag = false;
}
private void lerp() {
currentLerpTime += Time.deltaTime;
if (currentLerpTime > lerpTime)
{
currentLerpTime = lerpTime;
}
float perc = currentLerpTime / lerpTime;
transform.position = Vector3.Lerp(startPos, endPos, perc);
}
public void Update()
{
position = transform.localPosition;
if (drag)
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
float distance;
if (plane.Raycast(ray, out distance))
{
transform.position = ray.GetPoint(distance);
}
}
}
public void OnTriggerEnter(Collider other){
endPos = other.transform.position;
}
}
This code mostly works somewhat. I am able to drag the object around and it won't leave the plan it it resides on, for short distances. But, the further away I drag the object, then release it, the more likely it is that the object will not snap back to its original start position and will get stuck somewhere along the way. I am not sure if this is an issue with the Lerp() function or maybe how the plane it is dragged across is created. Can anyone help me out? If you need more info, please let me know. I've been working on this for weeks and haven't gotten much further that this.
Another issue that pops up, as well, is that when the object that is being dragged is released, and comes into contact with the hot spot's collider, it stops the object at its exact point. So, if a corner of the cube comes into contact with the collider, the cube will not come to rest centered on the hot spot.

Categories