I'm using Unity to develop a cross-platform application.
I'm using the following C# code to place a button on screen:
void onGUI()
{
float texWidth = m_buttonPano.normal.background.width;
float texHeight = m_buttonPano.normal.background.height;
float width = texWidth * Screen.width / 1920;
float height = (width / texWidth) * texHeight;
float y = Screen.height - height;
float x = Screen.width - width;
if (GUI.Button (new Rect (x, y, width, height), "", m_buttonPano)) {
if (this.TappedOnPanoButton != null) {
this.TappedOnPanoButton ();
}
m_guiInput = true;
}
}
Also note, that I added this script to my scene via creating an empty GameObject and attaching the script to it.
It works well on PC, but on Android the button doesn't show up. The interesting part is that if I tap at it's location (bottom right corner) the functionality is preserved, therefore it's only the custom background texture I put on it that doesn't show up..
ALso, here's the code of the attachment of the background texture:
m_buttonPano = new GUIStyle();
m_buttonPano.normal.background = Resources.Load("GUI/buttonPano") as Texture2D;
m_buttonPano.active.background = Resources.Load("GUI/buttonPano") as Texture2D;
m_buttonPano.onActive.background = Resources.Load("GUI/buttonPano") as Texture2D;
The problem was actually a Unity bug. After downloading the f4 patch, everything works correctly.
Related
I am currently developing a pixel art program in Unity. Obviously, it has a pencil tool with a script on it that I have made.
Unfortunately, the SetPixel method does not color the pixels. I don't know if it is the method itself that it's not working or something else.
This is the code I am using:
[SerializeField] private Sprite textureRendererSprite;
private Texture2D texture;
private MouseCoordinates mouseCoordinates;
void Start()
{
mouseCoordinates = GetComponent<MouseCoordinates>();
texture = textureRendererSprite.texture;
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
texture.SetPixel(int.Parse(mouseCoordinates.posInt.x.ToString()), int.Parse(mouseCoordinates.posInt.y.ToString()), Color.black);
Debug.Log(int.Parse(mouseCoordinates.posInt.x.ToString()));
Debug.Log(int.Parse(mouseCoordinates.posInt.y.ToString()));
}
}
Also, this is my MouseCoordinates script:
[SerializeField] private Canvas parentCanvas = null;
[SerializeField] private RectTransform rect = null;
[SerializeField] private Text text;
public Vector2 posInt;
[SerializeField] private Camera UICamera = null;
void Start()
{
if (rect == null)
rect = GetComponent<RectTransform>();
if (parentCanvas == null)
parentCanvas = GetComponentInParent<Canvas>();
if (UICamera == null && parentCanvas.renderMode == RenderMode.WorldSpace)
UICamera = parentCanvas.worldCamera;
}
public void OnPointerClick(PointerEventData eventData)
{
RectTransformUtility.ScreenPointToLocalPointInRectangle(rect, eventData.position, UICamera, out Vector2 localPos);
localPos.x += rect.rect.width / 2f;
localPos.y += rect.rect.height / 2f;
posInt.x = ((int)localPos.x);
posInt.y = ((int)localPos.y);
text.text = (posInt.x + ", " + posInt.y).ToString();
}
I was a little bored, so here is a fully working pixel draw I just whipped up. The one part you were missing with your implementation is Texture2D.Apply, which based on the Texture2D.SetPixels doc page,
This function takes a color array and changes the pixel colors of the
whole mip level of the texture. Call Apply to actually upload the
changed pixels to the graphics card.
Now to your actual implementation. You do not need a majority of the data you are caching, as a PointerEventData already has most of it. The only component you will need is the Image component that you want to change.
OnPointerClick is fine, but that only registers clicks, not dragging. If you want to make a pixel art tool, most art is done by dragging a cursor or stylus, so you will want to use an OnDragHandler instead or, along with your click.
One other note, you are not adding any brush size. More of a QoL update to your snippet, but with the addition of a brush size there are other complications that arise. SetPixel is bottom left aligned and must be contained within the bounds of the texture. You can correct this by offsetting the center point of your click by half a brush size, then clamping the width and height of your box.
Here is the current snippet:
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class TestScript : MonoBehaviour, IPointerClickHandler, IDragHandler
{
// color we are setting pixels to
[SerializeField] private Color clr = Color.white;
// our source UI image - it can be a raw image or sprite renderer, I just used UI image
[SerializeField] private Image img = null;
[Range(1, 255)]
[SerializeField] private int BrushSize = 1;
// the texture we are going to manipulate
private Texture2D tex2D = null;
private void Awake()
{
Sprite imgSprite = img.sprite;
// create a new instance of our texture to not write to it directly and overwrite it
tex2D = new Texture2D((int)imgSprite.rect.width, (int)imgSprite.rect.height);
var pixels = imgSprite.texture.GetPixels((int)imgSprite.textureRect.x,
(int)imgSprite.textureRect.y,
(int)imgSprite.textureRect.width,
(int)imgSprite.textureRect.height);
tex2D.SetPixels(pixels);
tex2D.Apply();
// assign this new texture to our image by creating a new sprite
img.sprite = Sprite.Create(tex2D, img.sprite.rect, img.sprite.pivot);
}
public void OnPointerClick(PointerEventData eventData)
{
Draw(eventData);
}
public void OnDrag(PointerEventData eventData)
{
Draw(eventData);
}
private void Draw(in PointerEventData eventData)
{
Vector2 localCursor;
// convert the position click to a local position on our rect
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(img.rectTransform, eventData.position, eventData.pressEventCamera, out localCursor))
return;
// convert this position to pixel coordinates on our texture
int px = Mathf.Clamp(0, (int)((localCursor.x - img.rectTransform.rect.x) * tex2D.width / img.rectTransform.rect.width), tex2D.width);
int py = Mathf.Clamp(0, (int)((localCursor.y - img.rectTransform.rect.y) * tex2D.height / img.rectTransform.rect.height), tex2D.height);
// confirm we are in the bounds of our texture
if (px >= tex2D.width || py >= tex2D.height)
return;
// debugging - you can remove this
// print(px + ", " + py);
// if our brush size is greater than 1, then we need to grab neighbors
if (BrushSize > 1)
{
// bottom - left aligned, so find new bottom left coordinate then use that as our starting point
px = Mathf.Clamp(px - (BrushSize / 2), 0, tex2D.width);
py = Mathf.Clamp(py - (BrushSize / 2), 0, tex2D.height);
// add 1 to our brush size so the pixels found are a neighbour search outward from our center point
int maxWidth = Mathf.Clamp(BrushSize + 1, 0, tex2D.width - px);
int maxHeight = Mathf.Clamp(BrushSize + 1, 0, tex2D.height - py);
// cache our maximum dimension size
int blockDimension = maxWidth * maxHeight;
// create an array for our colors
Color[] colorArray = new Color[blockDimension];
// fill this with our color
for (int x = 0; x < blockDimension; ++x)
colorArray[x] = clr;
// set our pixel colors
tex2D.SetPixels(px, py, maxWidth, maxHeight, colorArray);
}
else
{
// set our color at our position - note this will almost never be seen as most textures are rather large, so a single pixel is not going to
// appear most of the time
tex2D.SetPixel(px, py, clr);
}
// apply the changes - this is what you were missing
tex2D.Apply();
// set our sprite to the new texture data
img.sprite = Sprite.Create(tex2D, img.sprite.rect, img.sprite.pivot);
}
}
Here is a gif of the snippet in action. Quite fun to play around with. And remember, whatever texture you use for this must have the setting Read and Write enabled on the import settings. Without this setting, the data is not mutable and you can not access the texture data at runtime.
Edit: Skimmed your question a bit too quickly. Realizing you are using a 2D sprite and not a UI Image or RawImage. You can still draw to a Sprite, but as it is not a UI object, it does not have a RectTransform. However, in your second snippet you reference a RectTransform. Can you explain your setup a bit more? The answer I provided should be enough to point you in the right direction either way.
I want to create a new photo at the point I touch it and I want it to be done with every touch so I wrote the following line inside the void Update () function.
public Canvas cv;
public Image im;
I have defined the UI elements above.
for (var i = 0; i < Input.touchCount; ++i)
{ Touch touch = Input.GetTouch(i);
if (touch.phase == TouchPhase.Began)
{
Instantiate(im, Input.GetTouch(i).position, Quaternion.identity).transform.SetParent(cv.transform, false);
}
}
And when I try it with the unity remote app, I take the picture about 3-4 fingers above the point I click. what's the problem? please help!
Vector2 scaleSomething()
{
var screenPosition = Camera.main.WorldToScreenPoint(worldPosition);
var scaler = cv.GetComponentInParent<CanvasScaler>();
var guiScale = 1.0f;
if (Mathf.Approximately(scaler.matchWidthOrHeight, 0.0f))
guiScale = scaler.referenceResolution.x / (float) Screen.width;
else if (Mathf.Approximately(scaler.matchWidthOrHeight, 1.0f))
guiScale = scaler.referenceResolution.y / (float) Screen.height;
return new Vector2(
(screenPosition.x - (Screen.width* 0.5f)) * guiScale,
(screenPosition.y - (Screen.height* 0.5f)) * guiScale);
}
try removing the set parent part after instantiating
edit what is happening is when you instantiate your objects as children they get translated/scaled in local space to the parent. Because your canvas is getting stretched (scaled up on y) your children elements are also getting scaled up on y and are out of place.
I'm using Editor Window maybe that's the problem ?
The idea is when connecting two nodes is also to make an arrow at the end position that will show the connecting flow direction.
In the screenshot when I'm connecting two nodes for example Window 0 to Window 1
So there should be an arrow at the end of the line near Window 1 showing indicating that Window 0 is connected to Window 1 so the flow is from Window 0 to Window 1.
But it's not drawing any ArrowHandleCap.
I don't mind to draw another simple white arrow at the end position but it's not working at all for now. Not drawing an arrow at all.
This is my Editor Window code :
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using UnityEditor.Graphs;
using UnityEngine.UI;
public class NodeEditor : EditorWindow
{
List<Rect> windows = new List<Rect>();
List<int> windowsToAttach = new List<int>();
List<int> attachedWindows = new List<int>();
int tab = 0;
float size = 10f;
[MenuItem("Window/Node editor")]
static void ShowEditor()
{
const int width = 600;
const int height = 600;
var x = (Screen.currentResolution.width - width) / 2;
var y = (Screen.currentResolution.height - height) / 2;
GetWindow<NodeEditor>().position = new Rect(x, y, width, height);
}
void OnGUI()
{
Rect graphPosition = new Rect(0f, 0f, position.width, position.height);
GraphBackground.DrawGraphBackground(graphPosition, graphPosition);
int selected = 0;
string[] options = new string[]
{
"Option1", "Option2", "Option3",
};
selected = EditorGUILayout.Popup("Label", selected, options);
if (windowsToAttach.Count == 2)
{
attachedWindows.Add(windowsToAttach[0]);
attachedWindows.Add(windowsToAttach[1]);
windowsToAttach = new List<int>();
}
if (attachedWindows.Count >= 2)
{
for (int i = 0; i < attachedWindows.Count; i += 2)
{
DrawNodeCurve(windows[attachedWindows[i]], windows[attachedWindows[i + 1]]);
}
}
BeginWindows();
if (GUILayout.Button("Create Node"))
{
windows.Add(new Rect(10, 10, 200, 40));
}
for (int i = 0; i < windows.Count; i++)
{
windows[i] = GUI.Window(i, windows[i], DrawNodeWindow, "Window " + i);
}
EndWindows();
}
void DrawNodeWindow(int id)
{
if (GUILayout.Button("Attach"))
{
windowsToAttach.Add(id);
}
GUI.DragWindow();
}
void DrawNodeCurve(Rect start, Rect end)
{
Vector3 startPos = new Vector3(start.x + start.width, start.y + start.height / 2, 0);
Vector3 endPos = new Vector3(end.x, end.y + end.height / 2, 0);
Vector3 startTan = startPos + Vector3.right * 50;
Vector3 endTan = endPos + Vector3.left * 50;
Color shadowCol = new Color(255, 255, 255);
for (int i = 0; i < 3; i++)
{// Draw a shadow
//Handles.DrawBezier(startPos, endPos, startTan, endTan, shadowCol, null, (i + 1) * 5);
}
Handles.DrawBezier(startPos, endPos, startTan, endTan, Color.white, null, 5);
Handles.color = Handles.xAxisColor;
Handles.ArrowHandleCap(0, endPos, Quaternion.LookRotation(Vector3.right), size, EventType.Repaint);
}
}
The problem is that the arrow is always behind the e.g. Window 0 since you call DrawNodeWindow always after DrawNodeCurve.
It happens because the arrow is always drawen starting from the endPos to the right direction with length = size so you always overlay it with the window later ... you have to change
// move your endpos to the left by size
var endPos = new Vector3(end.x - size, end.y + end.height / 2 , 0);
in order to have it start size pixels left before the actual end.x position.
However, as you can see it is still really small since it usually is used to display the arrow in 3D space - not using Pixel coordinates .. you might have to tweak arround or use something completely different.
How about e.g. simply using a GUI.DrawTexture instead with a given Arrow sprite?
// assign this as default reference via the Inspector for that script
[SerializeField] private Texture2D aTexture;
// ...
// since the drawTexture needs a rect which is not centered on the height anymore
// you have to use endPos.y - size / 2 for the Y start position of the texture
GUI.DrawTexture(new Rect(endPos.x, endPos.y - size / 2, size, size), aTexture, ScaleMode.StretchToFill);
like mentioned in the comment for all serialized fields in Unity you can already reference default assets for the script itself (in contrary to doing it for each instance like for MonoBehaviours) so with the NodeEditor script selected simply reference a downloaded arrow texture
If using a white arrow as texture you could then still change its color using
var color = GUI.color;
GUI.color = Handles.xAxisColor;
GUI.DrawTexture(new Rect(endPos.x, endPos.y - size / 2, size, size), aTexture, ScaleMode.StretchToFill);
GUI.color = color;
Result
P.S.: Arrow icon usedfor the example: https://iconsplace.com/red-icons/arrow-icon-14 you can already change the color directly on that page before downloading the icon ;)
so I'm currently working on resolution independence for my game, and I'm testing it out on a sword image. The position changing is working, but whenever I start doing the size I end up with a blank screen.
These are the functions I run to get the new position and size of the sprite.
private static float CalcRatio(Vector2 size)
{
return size.Y / size.X;
}
public static Vector2 CalculateNewPos(Vector2 refPos, Vector2 refScreenSize, Vector2 currentScreenSize)
{
return new Vector2((refPos.X / refScreenSize.X) * currentScreenSize.X,
(refPos.Y / refScreenSize.Y) * currentScreenSize.Y);
}
public static Vector2 CalculateNewSize(Vector2 refSize, Vector2 refScreenSize, Vector2 currenScreenSize)
{
float origRatio = CalcRatio(refSize);
float perW = refSize.X * 100f / refScreenSize.X;
float newW = perW / 100f * currenScreenSize.X;
float newH = newW * origRatio;
return new Vector2(newW, newH);
}
In the Initialization function in Game1 I run this code:
graphics.PreferredBackBufferHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height;
graphics.PreferredBackBufferWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width;
swordPosition = CalculateNewPos(swordRefPosition, new Vector2(1920, 1080), new Vector2(GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width, GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height));
swordSize = CalculateNewSize(swordRefSize, new Vector2(1920, 1080), new Vector2(GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width, GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height));
In the load function I run this:
swordTexture = content.Load<Texture2D>("SOLDIER_Sword");
swordPosition = new Vector2(300, 0);
swordRefSize = new Vector2(557, 490);
swordSize = new Vector2(557, 490);
swordRefPosition = new Vector2(300, 0);
swordColor = Color.White;
sword = new StaticSprite(swordTexture, swordPosition, swordSize, swordColor);
In update everytime the screen resolution changes (I have buttons set to do that) this:
graphics.PreferredBackBufferHeight = setHeight;
graphics.PreferredBackBufferWidth = setWidth;
graphics.ApplyChanges();
swordPosition = CalculateNewPos(swordRefPosition, new Vector2(1920, 1080), new Vector2(setWidth, setHeight));
swordSize = CalculateNewSize(swordRefSize, new Vector2(1920, 1080), new Vector2(setWidth, setHeight));
And in draw:
batch.Draw(swordTexture, swordPosition, null, swordColor, 0f, Vector2.Zero, swordSize, SpriteEffects.None, 0f);
Sorry there is so much code, I'm really stumped and can't pinpoint where it's going wrong, so I just included everything that changes the variables.
Thank you so much for taking the time to look through this.
Note that current display mode is only meaningful if you are fullscreen. It's the VIEWPORT that tells you the width/height of spritebatch's projection matrix.
That said, you should really look into Bjarke's post, because you don't usually want to be rescaling every individual item.
I am trying to rotate a label through 90 degrees. At the moment I can take the text from the label and rotate that, but what I want to do is actually rotate a label of my choice, or if I were to be really flashy, lets say a button control. So using the code below, how can I modify it so that I can feed it a control and get it to rotate it?
protected override void OnPaint(PaintEventArgs e)
{
Graphics graphics = e.Graphics;
string text = label4.Text;
StringFormat stringFormat = new StringFormat();
stringFormat.Alignment = StringAlignment.Center;
stringFormat.Trimming = StringTrimming.None;
Brush textBrush = new SolidBrush(this.ForeColor);
//Getting the width and height of the text, which we are going to write
float width = graphics.MeasureString(text, this.Font).Width;
float height = graphics.MeasureString(text, this.Font).Height;
//The radius is set to 0.9 of the width or height, b'cos not to
//hide and part of the text at any stage
float radius = 0f;
if (ClientRectangle.Width < ClientRectangle.Height)
{
radius = ClientRectangle.Width * 0.9f / 2;
}
else
{
radius = ClientRectangle.Height * 0.9f / 2;
}
int rotationAngle = 90;
double angle = (rotationAngle / 180) * Math.PI;
graphics.TranslateTransform(
(ClientRectangle.Width + (float)(height * Math.Sin(angle)) - (float)(width * Math.Cos(angle))) / 2,
(ClientRectangle.Height - (float)(height * Math.Cos(angle)) - (float)(width * Math.Sin(angle))) / 2);
graphics.RotateTransform((float)rotationAngle);
graphics.DrawString(text, this.Font, textBrush, 0, 0);
graphics.ResetTransform();
}
Standard windows forms controls (such as a label and button) are rendered by the operating system itself, windows forms doesn't do the actual drawing.
Therefore, unfortunately, you have no control over aspects such as rotation and scaling with these sorts of controls. This is just a limitation of Windows Forms itself and is one of the major reasons Microsoft created WPF.
WPF controls are entirely rendered by WPF (using DirectX behind the scenes). WPF supports all the standard 2D (and 3D) transaformations such as scaling, rotation and translation.
Altrrnatively in windows forms you could create a custom control that you render using GDI+ and can rotate and scale as required. Of course now you're doing all the work yourself which it seems is not what you want.
You could use WPF instead of WinForms...then its a simple transform ;)