Hey,
I am working on a 3D room editor where you can grab an object from a menu and put it back in a room. You also get the option to give these objects a different color, this is done by the color picker. I now have a script that works the way I want it only if I enlarge the color picker, then he does not pick up the colors anymore and he does not move the selector circle anymore.
How do I solve this?
GIF Color picker at 1.8 scale
GIF Color picker at above 2 scale
Color picker script:
Color[] Data;
SpriteRenderer SpriteRenderer;
GameObject ColorPicker;
GameObject Selector;
BoxCollider Collider;
public GameObject target;
Ray rayray;
private Plane MyPlane;
public int Width { get { return SpriteRenderer.sprite.texture.width; } }
public int Height { get { return SpriteRenderer.sprite.texture.height; } }
public Color Color;
void Awake()
{
ColorPicker = transform.Find("ColorPicker").gameObject;
SpriteRenderer = ColorPicker.GetComponent<SpriteRenderer>();
Selector = transform.Find("Selector").gameObject;
Collider = ColorPicker.GetComponent<BoxCollider>();
Data = SpriteRenderer.sprite.texture.GetPixels();
Color = Color.white;
Debug.Log(Collider);
MyPlane = new Plane(transform.TransformDirection(Vector3.forward), transform.position);
}
void Update()
{
if (Input.GetMouseButton(0))
{
rayray = Camera.main.ScreenPointToRay(Input.mousePosition);
MyPlane = new Plane(transform.TransformDirection(Vector3.forward), transform.position);
Vector3 screenPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
screenPos = new Vector3(screenPos.x, screenPos.y);
//check if we clicked this picker control
RaycastHit[] ray = Physics.RaycastAll(rayray.origin, rayray.direction);
foreach (RaycastHit h in ray)
{
Debug.Log(h.collider.name);
if (h.collider.name == "ColorPicker")
{
Selector.transform.position = screenPos;
//get color data
screenPos -= ColorPicker.transform.position;
int x = (int)(screenPos.x * Width);
int y = (int)(screenPos.y * Height) + Height;
if (x > 0 && x < Width && y > 0 && y < Height)
{
Color = Data[y * Width + x];
target.GetComponent<Renderer>().material.color = Color;
Debug.Log(Width);
Debug.Log(Height);
}
}
}
}
}
EDIT:
GIF
This is the inspector from the color picker color field
This is the inspector from the main camera
IMAGE:
This is the inspector from the color picker color field
This is the inspector from the main camera
It is hard to know for sure without seeing your scene in the Unity editor but I have a few things I would check that may solve your problem.
Check to make sure that when you are scaling your GameObject that the BoxCollider is being scaled correctly. You should be able to see the Gizmo when selecting your GameObject after you scale it. Make sure it is covering the same areas of the UI before/after you scale it. The BoxCollider is used to detect mouse clicks on the object as a whole and unless the ray hits that collider, none of the other functionality will work.
It doesn't look like the script is taking into account scale changes for the color picker. Take a look where you get your X/Y coordinates for picking the color (under the //get color data comment). You will notice it multiplies the screen position by Height and Width which are taken from the size of your texture. This would need to be scaled accordingly to sample the correct area of the texture.
If you still can't get it to work, I would suggest posting a screen shot of your scene hierarchy and relevant GameObjects so we can see how it is all setup.
Related
I have currently started a developing a pixel art program in unity. I started with getting the mouse coordinates(x, y) on the ui image(texture) I used the code below but the get the mouse position in world space. I wanna get it in the space of the ui image only. The texture image is 32, 32 pixel wide and tall (x, y respected)
[SerializeField] private Text coordinatesText;
private Vector3 mousePos;
private float mouseX, mouseY;
void Update()
{
mousePos = Input.mousePosition;
mouseX = mousePos.x;
mouseY = mousePos.y;
coordinatesText.text = (mouseX + ", " + mouseY).ToString();
}
For like, when I hover the mouse on the very bottom left of the texture, which is a ui image, I get 0, 0 and when it's in the very top right, it's 32, 32.
Thanks in advance for your help!
Depending on the Render Mode your Canvas is set to, the answer differs ever so slightly. I also want to point out, RectTransform by default has a pivot point on its center, so the (0,0) will be in the center, not on the bottom-left.
The function you are looking for is RectTransformUtility. ScreenPointToLocalPointInRectangle. To use it, you need the RectTransform of your UI object, if the Render Mode of your Canvas is set to ScreenSpaceCamera, you will need the camera and the screen space position, which is the mouse position. I mentioned that it was different depending on the RenderMode due to the camera parameter needing to be null if your RenderMode is OverlaySpace for the function to work.
Another note, you will most likely want to implement IHandlers to know when the cursor is inside of the specific UI element.
Here is some example code:
using UnityEngine;
using UnityEngine.EventSystems;
public class TestScript : MonoBehaviour, IPointerClickHandler
{
[SerializeField] private Canvas parentCanvas = null; // the parent canvas of this UI - only needed to determine if we need the camera
[SerializeField] private RectTransform rect = null; // the recttransform of the UI object
// you can serialize this as well - do NOT assign it if the canvas render mode is overlay though
private Camera UICamera = null; // the camera that is rendering this UI
private void Start()
{
if (rect == null)
rect = GetComponent<RectTransform>();
if (parentCanvas == null)
parentCanvas = GetComponentInParent<Canvas>();
if (UICamera == null && parentCanvas.renderMode == RenderMode.ScreenSpaceCamera)
UICamera = parentCanvas.worldCamera;
}
public void OnPointerClick(PointerEventData eventData)
{
// this UI element has been clicked by the mouse so determine the local position on your UI element
RectTransformUtility.ScreenPointToLocalPointInRectangle(rect, eventData.position, UICamera, out Vector2 localPos);
// we now have the local click position of our rect transform, but as you want the (0,0) to be bottom-left aligned, need to adjust it
localPos.x += rect.rect.width / 2f;
localPos.y += rect.rect.height / 2f;
Debug.Log(localPos);
}
}
Here is a gif of the code working in a test scene. The image I have has a width and height of 32. I zoomed in the window so the clicks could be as closer to the edge to show the coordinates are properly set.
I'm using a Texture2D to display a map, and I need to get the color of the pixel I clicked on. I used Input.mousePosition to get the float coordinates, but using GetPixel to get the color requires the coordinates to be integers.
I am having trouble with getting GetPixel to find the coordinate that I clicked on.
When using floats and clicking on say, the rightmost side of the texture, I get a number like 27.xxx, but when I cast it to an integer, it displays a coordinate 27 pixels from the leftmost side of the texture. The way floats represent pixels confuses me a great deal, maybe clarifying that would help.
public class ProvinceSelectScript : MonoBehaviour {
public Material SpriteMain;
public Color SelectedCol;
public Color NewlySelectedCol;
public Texture2D WorldColMap;
Vector2 screenPosition;
Vector2 worldPosition;
void Start()
{
WorldColMap = (Texture2D)SpriteMain.GetTexture("_MainTexture");
NewlySelectedCol = Color.blue;
}
private void OnMouseDown()
{
screenPosition = new Vector2(Input.mousePosition.x,Input.mousePosition.y);
worldPosition = Camera.main.ScreenToWorldPoint(screenPosition);
SelectedCol = WorldColMap.GetPixel(((int)(worldPosition.x)+(WorldColMap.width/2)) , (int)((worldPosition.y)+(WorldColMap.height / 2)));
SpriteMain.SetColor("_SelectedProvince", SelectedCol);
SpriteMain.SetColor("_NewlySelectedProvince", NewlySelectedCol);
}
}
The worldPosition in the question isn't calculated in a way that's useful if you're using a perspective camera or if your camera is pointed any direction but directly forward.
To find the world position of the click, the best way to go about that is to use Camera.ScreenPointToRay to calculate the position of the click when intersecting the plane made by the position of the sprite and its local forward.
Either way, a world position does not mean anything to the sprite, which could be positioned anywhere in world space. You should rather use transform.InverseTransformPoint to calculate the local position you're clicking on. At that point, you can then use the spriterenderer's bounds to convert to normalized form (0-1 originating fromt he bottom-left instead of world unit lengths originating from the center).
But, once you have the local sprite position of the click expressed in normalized form, you can try to use GetPixelBilinear to get the color at the UV of (x,y) of the click. If the sprite is super simple, this MAY work. If it is animated or nine-sliced, or anything else it probably won't, and you'll have to reverse-engineer what UV the mouse is actually hovering over.
Camera mainCam;
SpriteRenderer sr;
void Start()
{
WorldColMap = (Texture2D)SpriteMain.GetTexture("_MainTexture");
NewlySelectedCol = Color.blue;
mainCam = Camera.main; // cache for faster access
sr = GetComponent<SpriteRenderer>(); // cache for faster access;
}
private void OnMouseDown()
{
Plane spritePlane = new Plane(transform.position, transform.forward);
Ray pointerRay = Camera.main.ScreenPointToRay(Input.mousePosition);
if (spritePlane.Raycast(pointerRay, out distance))
{
Vector3 worldPositionClick = pointerRay.GetPoint(distance);
Vector3 localSpriteClick = transform.InverseTransformPoint(worldPositionClick);
// convert [(-extents,-extents),(extents,extents)] to [(0,0),(1,1)]
Vector3 localSpriteExtents = sr.sprite.bounds.extents;
localSpriteClick = localSpriteClick + localSpriteExtents ;
localSpriteClick.x /= localSpriteExtents.x * 2;
localSpriteClick.y /= localSpriteExtents.y * 2;
// You clicked on localSpriteClick, on a very simple sprite (where no uv magic is happening) this might work:
SelectedCol = WorldColMap.GetPixelBilinear(localSpriteClick.x, localSpriteClick.y);
SpriteMain.SetColor("_SelectedProvince", SelectedCol);
}
I recently started using Unity and C# and am currently working on a Vertical 2D mobile Game. I'm struggling to get my background to scale with different aspect ratios. The background sprite is 19,5/9 and the playable area is 16/9. At the moment the background is scaling to fit the top and bottom of the screen, but the idea is to have the background anchored to the sides and bottom and for the view to extend upwards if needed (Hence the tall sprite). Any ideas? Thanks in advance.
Here is the code im trying, its attached to the Camera.
public SpriteRenderer background;
private void Start()
{
float screenRatio = (float)Screen.width / (float)Screen.height;
float targetRatio = background.bounds.size.x / background.bounds.size.y;
if(screenRatio >= targetRatio)
{
Camera.main.orthographicSize = background.bounds.size.y / 2;
}
else
{
float differenceInSize = targetRatio / screenRatio;
Camera.main.orthographicSize = background.bounds.size.y / 2 * differenceInSize;
}
}
My solution is to use a worldspace UI canvas.
Set the canvas to world space, place it at the desired depth in your scene (dimensions won't matter as we can set them in the script), and either add an image object as a child or add an image component to the canvas object. Add your sprite as the source for the image like so.
void Awake()
{
RectTransform rt = GetComponent<RectTransform>();
rt.position = new Vector3(0, 0, rt.position.z);
float camHeight = Camera.main.orthographicSize * 2;
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, camHeight);
float targetRectWidth = camHeight * Camera.main.aspect;
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, targetRectWidth);
}
The Steps are:
sets the rect transform to be positioned in the centre of the screen at whatever depth you set it at in the editor.
get 2xCamera height (as that is the distance from the top to bottom of the screen)
sets the anchors so that it lines the UI object up with the top and bottom of the screen
gets the target width by multiplying the height by the aspect ratio of the camera
sets the correct width based the target width
This can be done in update instead of Awake or Start to dynamically size the background while playing if necessary.
Here it is at 1080p 16:9 and at 5:4.The red cube is to show that it is in the background behind objects in the scene.
I'm making my first game where obstacles (which are prefabs) are placed by a script into a scene. They are all different sizes in a 2D environment. I am placing them using this code below
Instantiate(normal1, new Vector3(x, y , 0), Quaternion.identity);
This works perfectly, except that I need all of the obstacles to be positioned from the top left. So if I would have 0, 0 for my x and y, only the obstacle's corner would be covering the 0, 0 position, not the entire thing. From the little I've worked with GUI elements, you can align to however you like. Is this the same with prefab, and if so, how? Or can you somehow move the origin of the object?
I assume you are talking about non-UI elements.
The easiest thing would be to give your objects a parent GameObject and arrange them in a way so the parent GameObject already has the pivot where you want it (the top-left corner). You do this by first positioning the (future) parent object correctly and then simply drag the child object into it in the hierachy (while it keeps its current position).
Then in your script you have to use Camera.ScreenToWorldPoint in order to find the top-left screen corner position in the 3D world like
// an optional offset from the top-left corner in pixels
Vector2 PixelOffsetFromTopLeft;
// Top Left pixel is at
// x = 0
// y = Screen height
// and than add the desired offset
var spawnpos = new Vector2(0 + PixelOffsetFromTopLeft.x, Screen.height - PixelOffsetFromTopLeft.y);
// as z we want a given distance to camera (e.g. 2 Units in front)
spawnedObject = Instantiate(Prefab, camera.ScreenToWorldPoint(new Vector3(spawnpos.x, spawnpos.y, 2f)), Quaternion.identity);
Full script I used as example
public class Spawner : MonoBehaviour
{
public GameObject Prefab;
public Vector2 PixelOffsetFromTopLeft = Vector2.zero;
private GameObject spawnedObject;
private Camera camera;
private void Start()
{
camera = Camera.main;
}
private void Update()
{
if (Input.anyKeyDown)
{
// you don't need this I just didn't want to mess up the scene
// so I just destroy the last spawned object before placing a new one
if (spawnedObject)
{
Destroy(spawnedObject);
}
// Top Left pixel is at
// x = 0
// y = Screen height
// and than add the desired offset
var spawnpos = new Vector2(0 + PixelOffsetFromTopLeft.x, Screen.height - PixelOffsetFromTopLeft.y);
// as z we want a given distance to camera (e.g. 2 Units in front)
spawnedObject = Instantiate(Prefab, camera.ScreenToWorldPoint(new Vector3(spawnpos.x, spawnpos.y, 2f)), Quaternion.identity);
}
}
}
This will now always spawn the object anchored to the top left. It will not keep it this way if the camera moves or the window is resized.
If your question was about keeping that position also when the window is resized you can use the same code e.g. in an Update method (later you due to efficiency you should only call it when really needed / window actually was resized) like
public class StickToTopLeft : MonoBehaviour
{
private Camera camera;
public Vector2 PixelOffsetFromTopLeft = Vector2.zero;
private void Start()
{
camera = Camera.main;
}
// Update is called once per frame
private void Update()
{
var spawnpos = new Vector2(0 + PixelOffsetFromTopLeft.x, Screen.height - PixelOffsetFromTopLeft.y);
// This time simply stay at the current distance to camera
transform.position = camera.ScreenToWorldPoint(new Vector3(spawnpos.x, spawnpos.y, Vector3.Distance(camera.transform.position, transform.position)));
}
}
This now keeps the object always anchored to the top-left corner also after resizing the window and allows to adjust its offset afterwards.
I have certain amount of sprites in my scene. It is a 2D Game. I want to display some text on the sprite each Sprite's GameObject is attached with a Script. In the Script I have the following OnGUI() Method
void OnGUI ()
{
GUI.contentColor = Color.black;
Rect label = new Rect ();
label.x = worldToRect (transform.position).x;
label.y = worldToRect (transform.position).y;
label.width = 100;
label.height = 100;
GUI.Label (label, number.ToString ());
}
Vector2 worldToRect (Vector3 WorldPosition)
{
Vector2 v = Camera.main.WorldToScreenPoint (WorldPosition);
return GUIUtility.ScreenToGUIPoint (v);
}
But the position is not quite right. The two white Sprites should display 3 at the centre.I checked their transform in the editor and they align exactly at the centre of the sprite. How do I correct this ?
The x and y in the Rect for the label are the top-left position, which is aligned with the center of the GameObjects.
To center them, subtract half the width from x and half the height from y, and make sure the label's style has Alignment set to "Middle Center"