C# Xna achieve a desent scale effect with sprite fonts - c#

So I am working on a menu for my game. Here's the code for a single button:
// button class
public class ButtonGUI
{
public SpriteFont spriteFont;
string btnTxt;
public Rectangle btnRect;
Color colour;
public ButtonGUI(string newTxt, Rectangle newRect, SpriteFont newSpriteFont, Color newColour)
{
spriteFont = newSpriteFont;
btnRect = newRect;
btnTxt = newTxt;
colour = newColour;
}
public void Draw(SpriteBatch spriteBatch)
{
// TextOutliner() is a static helper class for drawing bordered spritefonts I made
TextOutliner.DrawBorderedText(spriteBatch, spriteFont, btnTxt, btnRect.X, btnRect.Y, colour);
}
}
// TitleScreen.cs
ButtonGUI btnPlay, btnPlay_2;
bool indexPlay;
string[] menuTxt;
SpriteFont GameFontLarge, GameFontLargeHover;
// LoadContent() method:
// Load both spritefonts....
menuTxt = new string[4];
menuTxt[0] = "Play Game";
menuTxt[1] = "Achievements";
menuTxt[2] = "Settings";
menuTxt[3] = "Exit Game";
btnPlay = new ButtonGUI(menuTxt[0], new Rectangle(150, 300, (int)GameFontLarge.MeasureString(menuTxt[0]).X, (int)GameFontLarge.MeasureString(menuTxt[0]).Y), GameFontLarge, Color.White);
btnPlay_2 = new ButtonGUI(menuTxt[0], new Rectangle(150, 300, (int)GameFontLargeHover.MeasureString(menuTxt[0]).X, (int)GameFontLargeHover.MeasureString(menuTxt[0]).Y), GameFontLargeHover, Color.Yellow);
// Update() method:
MouseState mouseState = Mouse.GetState();
Rectangle mouseRect = new Rectangle(mouseState.X, mouseState.Y, 1, 1);
if (mouseRect.Intersects(btnPlay.btnRect))
{
indexPlay = true;
if (mouseState.LeftButton == ButtonState.Pressed) Game1.CurrentGameState = Game1.GameState.playScreen;
}
else indexPlay = false;
// Draw() method:
if (indexPlay)
{
btnPlay_2.Draw(spriteBatch);
}
else btnPlay.Draw(spriteBatch);
So I do all of this for 4 different buttons. The 2 sprite fonts are of the same font but with different size. Now when I test the game, when I mouse-hover on each of the buttons, the text changes from white to yellow and becomes larger. But since the button coordinates are done with x = left-most; y = top-most when the font changes the bigger font is drawn at the same position as the smaller one was. I want to get a decent "scale" effect when mouse-hovering. I mean I know I set the button position to that when initializing, but still I need some type of algorithm to find a way and draw the bigger font on the center of the older... What would be your approach?

You could change the position of your hover button based on the size difference:
var sizeDifference = btnPlay_2.btnRect.Size - btnPlay.btnRect.Size;
btnPlay_2.btnRect.Offset(-sizeDifference.X/2, -sizeDifference.Y/2);

Related

Unity's SetPixel method does not color out the given pixels

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.

Unity canvas scaler blocking GL.Vertex

So I'm drawing lines using GL in Unity. I draw those lines on my screen, and they are being drawn. However, I only see the lines once I disable the canvas scaler on the script that is providing the data for the lines.
I tried other ways of drawing the lines but nothing helped.
So the setup is as followed, I have a canvas in my scene with a canvas scaler ontop of it that scales with screen size. This canvas contains a script that provides the data (positions) for the lines. When the lines need to be drawn, it fires an event with a vector2[] with all the data that needs lines etc. I have another script on my main camera listening to this event, and when needed it draws the lines in the onpostrender. These lines are being drawn at the correct positions, but I can only see them once I disable the canvas scaler on the canvas. Here's some code:
This is the script in my scene (thats on the canvas) that fires an event with the data.
lock (_skeleton2D)
{
for (int i = 0; i < skeleton.Length; ++i)
{
_skeleton2D[i] = new Vector2(skeleton[i].x * scaleX + _depthImageRect.anchoredPosition.x * 2, _depthImageRect.rect.height - skeleton[i].y * scaleY);
}
for (int i = 0; i < skeleton.Length; i++)
{
Vector2 vector2 = skeleton[i];
objects[i].transform.position = vector2;
// _skeleton2D[i] = vector2;
}
DrawSkeletonRenderer?.Invoke(_skeleton2D);
As you can see in the end it fires the DrawSkeletonRenderer with a Vector2[] _skeleton2D. Then I have a script on the Main and only camera in the scene which is listening to this event, and when it receives the event, it updates it's own Vector2[] of skeletondata, and uses this to render the lines.
private void Start()
{
_skeletonMaterial = new Material(Shader.Find("UI/Default"));
MoveCalibrationToolManager.DrawSkeletonRenderer += receivedDrawSkeleton;
}
private void receivedDrawSkeleton(Vector2[] skeleton)
{
_skeleton2D = skeleton;
isSkeletonValid = true; //TODO Turn false when not needed anymore.
}
private bool isSkeletonValid;
private void OnPostRender()
{
if (isSkeletonValid)
{
lock (_skeleton2D)
{
drawSkeleton(_skeleton2D);
Debug.LogError("DRAW SKELETON RIGHT NOW!");
}
}
}
As you can see it eventually calls drawSkeleton which as shown below draws the lines
GL.PushMatrix();
_skeletonMaterial.SetPass(0);
GL.LoadPixelMatrix();
drawBone(skeleton[(int)JointType.Head], skeleton[(int)JointType.Neck]);
drawBone(skeleton[(int)JointType.Neck], skeleton[(int)JointType.LShoulder]);
drawBone(skeleton[(int)JointType.Neck], skeleton[(int)JointType.RShoulder]);
drawBone(skeleton[(int)JointType.LShoulder], skeleton[(int)JointType.LElbow]);
drawBone(skeleton[(int)JointType.LElbow], skeleton[(int)JointType.LHand]);
drawBone(skeleton[(int)JointType.RShoulder], skeleton[(int)JointType.RElbow]);
drawBone(skeleton[(int)JointType.RElbow], skeleton[(int)JointType.RHand]);
drawBone(skeleton[(int)JointType.LShoulder], skeleton[(int)JointType.RWaist]);
Which then calls drawBone which looks as follows:
if (float.IsNaN(fromJoint.x) || float.IsNaN(fromJoint.y) || float.IsNaN(toJoint.x) || float.IsNaN(toJoint.y))
{
Debug.Log("Joint positions for lines are null");
return;
}
GL.Begin(GL.LINES);
GL.Color(Color.red);
GL.Vertex(fromJoint);
GL.Vertex(toJoint);
GL.End();
As I said, everything is drawn but I can only see it when I disable the Canvas Scaler on the canvas that has the first script attached.
During runtime, I make a texture on which I show a feed of a 3D realsense camera. I need the skeleton to be drawn ontop of that. I make this texture the following way:
GameObject obj = new GameObject("Calibration View");
obj.transform.SetParent(gameObject.transform, false);
obj.transform.SetAsFirstSibling();
_depthImage = obj.AddComponent<RawImage>();
_depthImageRect = obj.GetComponent<RectTransform>();
_depthImageRect.anchorMin = Vector2.zero;
_depthImageRect.anchorMax = Vector2.one;
_depthImageRect.anchoredPosition = Vector2.zero;
Quaternion transformRotation = new Quaternion(180f, 0f, 0f, 0f);
_depthImageRect.transform.rotation = transformRotation;
_depthImage.material = _depthImageMaterial;
_depthTexture = new Texture2D(RsDevice.DepthWidth, RsDevice.DepthHeight, TextureFormat.BGRA32, false);
_depthImage.texture = _depthTexture;
_depthTextureBytes = new byte[RsDevice.DepthWidth * RsDevice.DepthHeight * 4];

Assigning property through constructor overloading

In the following class, I want to assign color as Color.White for constructor 1; and when called through constructor 2, it should be assigned the parameter value. But in doing that, it first calls the constructor 1 which in turn first assigns color as Color.White and then the required value is assigned.
The problem becomes reasonable when many constructors are involved and objects are included.
Is there any way to deal with such unnecessary steps? I guess I'm missing something fundamental here.
public class Image
{
Texture2D texture;
Rectangle frame;
Rectangle offsetBound;
Color color;
// Constructor 1
public Image(Texture2D texture, Rectangle frame, Rectangle offsetBound)
{
this.texture = texture;
this.frame = frame;
this.offsetBound = offsetBound;
this.color = Color.White; // This is irrelevant
}
// Constructor 2
public Image(Texture2D texture, Rectangle frame, Rectangle offsetBound, Color color)
: this(texture, frame, offsetBound)
{
this.color = color;
}
}
You could rearrange things like this:
// Constructor 1
public Image(Texture2D texture, Rectangle frame, Rectangle offsetBound)
: this(texture, frame, offsetBound, Color.White)
{ }
// Constructor 2
public Image(Texture2D texture, Rectangle frame, Rectangle offsetBound, Color color)
{
this.texture = texture;
this.frame = frame;
this.offsetBound = offsetBound;
this.color = color;
}
You can also do the next,eliminating the first constructor just leaving you with one constructor like so will provide the same outcome:
public Image(Texture2D texture, Rectangle frame, Rectangle offsetBound, Color? col = null)
{
this.color = col ?? Color.White;
this.texture = texture;
this.frame = frame;
this.offsetBound = offsetBound;
}
By using optional parameters you gain the same outcome as with your 2 ctors,if the user does not want to provide a color simply dont and it will be placed the default of Color.White.

Windows Phone XNA animation

I have VS2012 and try to make a simple xna app for Windows Phone 7/8.
I have something like this:
public partial class GamePage : PhoneApplicationPage
{
ContentManager contentManager;
GameTimer timer;
SpriteBatch spriteBatch;
public Texture2D firstSprite { get; set; }
public Vector2 transition = new Vector2(0, 0);
public GamePage()
{
InitializeComponent();
contentManager = (Application.Current as App).Content;
timer = new GameTimer();
timer.UpdateInterval = TimeSpan.FromTicks(333333);
timer.Update += OnUpdate;
timer.Draw += OnDraw;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(true);
spriteBatch = new SpriteBatch(SharedGraphicsDeviceManager.Current.GraphicsDevice);
firstSprite = this.contentManager.Load<Texture2D>("ikona");
timer.Start();
base.OnNavigatedTo(e);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
timer.Stop();
SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(false);
base.OnNavigatedFrom(e);
}
float d = 0;
private void OnUpdate(object sender, GameTimerEventArgs e)
{
TouchCollection touchCollection = TouchPanel.GetState();
foreach (TouchLocation tl in touchCollection)
{
if (tl.State == TouchLocationState.Pressed && tl.Position.X < 240)
{
d = -10;
}
if (tl.State == TouchLocationState.Pressed && tl.Position.X > 240)
{
d = 10;
}
if (tl.State == TouchLocationState.Released)
d = 0;
}
transition += new Vector2(d, 0);
}
private void OnDraw(object sender, GameTimerEventArgs e)
{
SharedGraphicsDeviceManager.Current.GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
spriteBatch.Draw(firstSprite, transition, null, Color.White, 0, new Vector2(0, 0), 1f, SpriteEffects.None, 0);
spriteBatch.End();
}
}
Is it a good way do animate sprite? Will it be animated same way on every device? When I was testing it on lumia920 it wasn't very smooth, so I think I do it badly.
And second thing. I want to move picture left when I press left half of the screen and move right when I press right side. It partly works but when I press left (it moves left), then at the same time press right (it moves right), and then release right, picture stops. I thought it would move again left. How can I achieve this?
/edit/
And what about touch handling?
I'm not sure how it's animated as I'm not familiar with TouchCollection.
But using XNA to animate works like this for most of the solutions out there:
You have your texture which is a spritesheet I assume containing all the frames for your animated sprite.
You use a Rectangle which will hold Position and size on the screen.
You also use something called a source rectangle, which is a rectangle that represents an area inside your sprite, which will stretch in your position rectangle. Here is an image:
Rectangle A is your position + size rectangle, and rectangle B is your source rectangle.
To create an animation you say the following ( pseudo code ):
int timer = 0;
Rectangle position = new Rectangle(100, 100, 80, 80); // position 100x, 100y, and size is 80 width and height.
Rectangle source = new Rectangle(0, 0, 80, 80); // Position on the spritesheet is 0, 0 which should be first frame, and the frames are all 80*80 pixels in size.
private void OnUpdate(object sender, GameTimerEventArgs e)
{
timer += e.ElapsedTime.TotalMilliseconds;
if(timer > 30) // If it has been 0.03 seconds = 33 frames per second
{
timer = 0; // reset timer
source.X += 80; // Move x value to next frame
if(source.X > widthOfYourSpriteSheet) source.X = 0; // Reset the animation to the beginning when we are at the end
}
}
Your draw function will look pretty much the same, except for the sourcerectangle argument you will put in it.

How can I get an IBeam cursor in an XNA Game?

I've managed to get the cursor to change to an IBeam in a trivial XNA 'Game' with Update as:
protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
// TODO: Add your update logic here
if (Keyboard.GetState().IsKeyDown(Keys.A))
{
System.Windows.Forms.Cursor.Current = System.Windows.Forms.Cursors.Arrow;
}
if (Keyboard.GetState().IsKeyDown(Keys.I))
{
System.Windows.Forms.Cursor.Current = System.Windows.Forms.Cursors.IBeam;
}
base.Update(gameTime);
}
The mouse changes to the IBeam cursor when 'I' is pressed, but immediately changes back to an arrow when you move the mouse. Is there a way to get it to stay as the default Windows IBeam, or will I need to create and track a custom cursor?
[EDIT] I should also point out that setting the cursor every single frame causes it to flicker when the mouse is moved. It seems the XNA (or Windows) is internally resetting the cursor to an arrow every frame?
Before resorting to drawing it manually, I'd try to set the Cursor property of the underlying System.Windows.Forms.Form:
Form f = (Form)Control.FromHandle(Game.Window.Handle);
f.Cursor = System.Windows.Forms.Cursors.IBeam;
As I don't have XNA installed right now, I don't know if this will work, or if it will be permenant, but I don't see why it wouldn't.
Sounds like your going to have to draw it manually. It shouldn't be too hard - just draw a sprite via SpriteBatch at the location of the cursor.
// ex.
protected override void Draw(GameTime gameTime)
{
// Other stuff...
base.Draw(gameTime);
// Retrieve mouse position
MouseState mState = Mouse.GetState();
Vector2 mousePos = new Vector2(mState.X, mState.Y);
// Use this instead to optionally center the texture:
// Vector2 mousePos = new Vector2(mState.X - cursorTexture.Width / 2, mState.Y - cursorTexture.Height / 2);
// Draw cursor after base.Draw in order to draw it after any DrawableGameComponents.
this.spriteBatch.Begin(); // Optionally save state
this.spriteBatch.Draw(cursorTexture, mousePos, Color.White);
this.spriteBatch.End();
}
And here's some code to extract the cursor image. Keep in mind you'll need an extra reference to System.Drawing.
protected override void LoadContent()
{
// Other stuff...
// The size of the cursor in pixels.
int cursorSize = 32;
// Draw the cursor to a Bitmap
Cursor cursor = Cursors.IBeam;
Bitmap image = new Bitmap(cursorSize, cursorSize);
Graphics graphics = Graphics.FromImage(image);
cursor.Draw(graphics, new Rectangle(0, 0, cursorSize, cursorSize));
// Extract pixels from the bitmap, and copy into the texture.
cursorTexture = new Texture2D(GraphicsDevice, cursorSize, cursorSize);
Microsoft.Xna.Framework.Graphics.Color[] data = new Microsoft.Xna.Framework.Graphics.Color[cursorSize * cursorSize];
for (int y = 0; y < cursorSize; y++)
{
for (int x = 0; x < cursorSize; x++)
{
System.Drawing.Color color = image.GetPixel(x, y);
data[x + y * cursorSize] = new Microsoft.Xna.Framework.Graphics.Color(color.R, color.G, color.B, color.A);
}
}
cursorTexture.SetData<Microsoft.Xna.Framework.Graphics.Color>(data);
}
Keep in mind this is off the top of my head - not guaranteed to work. The basic idea is there, though.

Categories