I have a code for a crafting system that checks if the inventory has the ingredients needed to craft an item and adds a button to craft it. The problem is when I want to position my button it goes way off the canvas. I have seen some people saying that it has something to do with rect transform. I've been stuck with it for over an hour. Any help is appreciated.
I have tried
removing the setparent() function,
using anchoredPosition,
using localPosition
My code
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Crafting : MonoBehaviour
{
public List<recipe> recipes = new List<recipe>();
public GameObject base_item, parent;
List<GameObject> items = new List<GameObject>();
public int y = 75;
public int x = -45;
public Inv inv;
private void Start()
{
inv = GetComponent<Inv>();
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Tab))
{
checkitems();
Debug.Log("y = " + y + " x = " + (x - 40));
}
}
public void checkitems()
{
for (int i = 0; i < recipes.Count; i++)
{
recipe r = recipes[i];
for (int x = 0; x < r.ingredients.Count; x++)
{
if (!inv.hasitem(r.ingredients[x])){
return;
}
}
showitem(r.result);
}
}
public void onClick(int _slot)
{
recipe r = recipes[_slot];
for (int i = 0; i < r.ingredients.Count; i++)
{
inv.removeitem(inv.getitem(r.ingredients[i]));
}
inv.additem(inv.getFirstAvailable(), r.result, r.stack);
}
public void showitem(string name)
{
GameObject obj = Instantiate(base_item);
if (items.Count != 0)
{
if (((items.Count) % 3) != 0)
{
Debug.Log("first thing");
obj.GetComponent<RectTransform>().position = new Vector2(x, y);
obj.transform.SetParent(parent.transform);
obj.SetActive(true);
items.Add(obj);
x = x + 40;
Debug.Log("x + 40");
}
else if (((items.Count + 1) % 3) == 0)
{
Debug.Log("second thing");
x = -45;
Debug.Log("x + 40");
y = y + 40;
Debug.Log(" y + 40");
obj.GetComponent<RectTransform>().position = new Vector2(x, y);
obj.transform.SetParent(parent.transform);
obj.SetActive(true);
items.Add(obj);
}
}else
{
obj.GetComponent<RectTransform>().position = new Vector2(x, y);
obj.transform.SetParent(parent.transform);
obj.SetActive(true);
items.Add(obj);
x = x + 40;
Debug.Log("x + 40");
}
}
}
Blue circle where it spawns. Red circle where I want it to be
Seems you are confusing a bunch of terms for being the issue of your problem. Firstly I want to address the red X over your scroll bar. Whenever this occurs, it means that your RectTransform of this UI object has been dragged from its positive vertices to negative or vice versa, causing it to almost invert. I would correct this but it is not the reason your objects are not childing correctly.
Generally, with UI objects, I would never use LocalPosition, just AnchoredPosition. LocalPosition is a field from Transform which I believe RectTransform inherits from. As RectTransforms have a lot of modifications to their position from pivots, anchors, and anchored positions, the LocalPosition will most likely need to recalculate data to properly move the object, whereas AnchoredPosition has already done these calculations.
I believe the issue with your current code is how you are using SetParent. There is a second parameter of SetParent which governs whether the object keeps the same position based in world space after being childed. As you are not passing in a new bool for this parameter, it is defaulting to true. As you want your objects to be childed to the parent but not keep their world space positions, you would want to pass in false.
In your case, as it looks as if you want to set objects in a grid-like pattern childed to this ScrollRect, I would attach a GridLayoutGroup to the Content of your scroll and child the new objects to this object. You can set the max columns of this grid and spacing to give the same layout you are attempting to achieve in code.
To summarize, I would remove all the hand placement you are doing in code with LocalPosition and AnchorPosition and just attach a GridLayoutGroup. To fix the current positioning of your objects relative to the parent, change all lines of obj.transform.SetParent(parent.transform); to obj.transform.SetParent(parent.transform, false);. If you want to keep changing position locally in code instead of a layout element, use SetParent first, and use AnchoredPosition instead of LocalPosition as the SetParent with false passed in will override the position you set.
Related
I want to change rect transform top value of GameObject(ui pannel) dependely of Instantiated objects in it.
I found that GameObject.transform.localPosition is not useful for that. localScale stratch the elements inside of scaled GameObject.
How it can be done correctly??
public void OnMouseEnter()
{
{
ResoursesTipsNewPannel = Instantiate(ResoursesTipsPanel);
ResoursesTipsNewPannel.transform.SetParent(ResoursesPanelTransform, false);
for (i = 0; i < n; i++)
{
//RIGHT HERE I GUESS MUST BE SOME CODE THAT CHANGE TOP VALUE OF MY PANNEL
ResoursesNewTipText = Instantiate(ResoursesTipText);
ResoursesNewTipText.transform.SetParent(ResoursesTipsNewPannel.transform, false);
ResoursesNewTipText.text = "Exemple text, " + i;
}
i = 0;
}
}
Maybe some existing component that is attached to "ResoursesTipsNewPannel"(from example code) that change scale of panel automatically?
To solve I added this into code from my example:
RectTransform ChangeRectTransform;
ChangeRectTransform = ResoursesTipsNewPannel.GetComponent<RectTransform>();
ChangeRectTransform.offsetMax = new Vector2(ChangeRectTransform.offsetMax.x, ChangeRectTransform.offsetMax.y+20);
I am attempting to use a custom FlowLayoutGroup as described in the answers of this question ( also up on GitHub) in a situation where it needs to resize vertically to contain it's children.
My setup looks like this:
ScrollableRect
Panel with VerticalLayoutGroup comp (content of parent scrollrect) that should resize vertically to fit children:
Panel with FlowLayoutGroup that should resize vertically to fit children
Panel with FlowLayoutGroup (2) also must resize...
etc...
I have added a content size fitter to the FlowLayoutGroup, tweaked the layout child size controls of the vertical group, but with no success.
The user may add and remove children of the groups while the app is running and I want the UI to respond so it is not possible to set the height of everything in advance.
I have also looked in the unity source code to try and figure out how to write this into the component myself. This is looking the best bet but taking me considerable time as I'm new to Unity and C#. Hoping someone has solved a similar problem already.
Everything functions as desired/expected except for the missing behaviour of LayoutGroups resizing to fit their children vertically.
How can I do this?
After some time and a tumbleweed badge I've decided to put the time in to make a solution, hopefully someone else benefits too.
Again, this is a modified version of the work done here. Thanks for that. This component now computes it's own preferred size.
Main changes:
I stripped it back quite severely:
All horizontal overrides are emptied, I only need the horizontal wrapping behaviour
Removed some apparent hangover variables from GridLayout class
Logic to calculate child positions and in turn number of rows, preferred height is in it's own method.
Child positions are stored in an Vector2 array to separate calculation from child setting.
This fixes the problem of height of the entire component not adjusting, it also immediately responds, with the original script because of the way children rectTransforms were set then accessed the script took two 'cycles' to recognize the dimensions of a child.
This suits all my needs, I imagine it can be fairly easily reworked to handle vertical wrap too...
using UnityEngine;
using UnityEngine.UI;
[AddComponentMenu("Layout/Wrap Layout Group", 153)]
public class WrapLayoutGroup : LayoutGroup
{
[SerializeField] protected Vector2 m_Spacing = Vector2.zero;
public Vector2 spacing { get { return m_Spacing; } set { SetProperty(ref m_Spacing, value); } }
[SerializeField] protected bool m_Horizontal = true;
public bool horizontal { get { return m_Horizontal; } set { SetProperty(ref m_Horizontal, value); } }
private float availableWidth { get { return rectTransform.rect.width - padding.horizontal + spacing.x; } }
private const float MIN_HEIGHT = 80;
private int preferredRows = 1;
private float calculatedHeight = MIN_HEIGHT;
private Vector2[] childPositions = new Vector2[0];
protected WrapLayoutGroup()
{ }
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
}
#endif
public override void CalculateLayoutInputVertical()
{
calculatePositionsAndRequiredSize();
SetLayoutInputForAxis(calculatedHeight, calculatedHeight, -1, 1);
}
public override void SetLayoutHorizontal() { }
public override void SetLayoutVertical()
{
SetChildren();
}
private void SetChildren()
{
for (int i = 0; i < rectChildren.Count; i++)
{
RectTransform child = rectChildren[i];
SetChildAlongAxis(child, 0, childPositions[i].x, LayoutUtility.GetPreferredWidth(child));
SetChildAlongAxis(child, 1, childPositions[i].y, LayoutUtility.GetPreferredHeight(child));
}
}
private void calculatePositionsAndRequiredSize()
{
childPositions = new Vector2[rectChildren.Count];
Vector2 startOffset = new Vector2(
GetStartOffset(0, 0),
GetStartOffset(1, 0)
);
Vector2 currentOffset = new Vector2(
startOffset.x,
startOffset.y
);
float childHeight = 0;
float childWidth = 0;
float maxChildHeightInRow = 0;
int currentRow = 1;
for (int i = 0; i < rectChildren.Count; i++)
{
childHeight = LayoutUtility.GetPreferredHeight(rectChildren[i]);
childWidth = LayoutUtility.GetPreferredWidth(rectChildren[i]);
//check for new row start
if (currentOffset.x + spacing.x + childWidth > availableWidth && i != 0)
{
currentOffset.x = startOffset.x;
currentOffset.y += maxChildHeightInRow + spacing.y;
currentRow++;
maxChildHeightInRow = 0;
}
childPositions[i] = new Vector2(
currentOffset.x,
currentOffset.y
);
//update offset
maxChildHeightInRow = Mathf.Max(maxChildHeightInRow, childHeight);
currentOffset.x += childWidth + spacing.x;
}
//update groups preferred dimensions
preferredRows = currentRow;
calculatedHeight = currentOffset.y + maxChildHeightInRow + padding.vertical - spacing.y;
}
}
I am trying to program a Unity Minesweeper game, and I am almost done, but as part of it I have a boolean variable called "isCovered" that works out if the square has been clicked or not.
This variable is used to see if the player has won yet. I have a 2d array that stores all of my squares, and to see if you win yet, it checks the array for covered, non mine squares, and if it finds them, concludes that you have not won yet.
However, when I test it, the code does not seem to find any squares that are covered, even though I can see that there are some. I concluded this because it outputs the "you win" message every time I click a safe square, even when I am not finished.
My variable is declared using this code:
public class Square : MonoBehaviour {
//creates boolean variable
public bool IsCovered() {
//checks if the square still uses the default texture.
return GetComponent<SpriteRenderer>().sprite.texture.name == "Default";
}
}
and the code to determine if you win, which is within a different class (that is my array/grid) is:
public static bool gameWon()
{
// Checks all squares in the array
foreach (Square elem in elements)
// Tries to find a covered, safe square
if (elem.IsCovered() && !elem.dangerous)
// The player is not finished
return false;
// The player has won
return true;
}
The asset that is used for the default texture is called "Uncleared proposed", rather than "Default" so I don't know if that is an issue.
My entire "Square" code for each individual tile is:
using System.Collections;
using UnityEngine;
public class Square : MonoBehaviour
{
//is this dangerous?
public bool dangerous;
//creates boolean variable
public bool IsCovered()
{
//checks if the square still uses the default texture.
return GetComponent<SpriteRenderer>().sprite.texture.name == "Default";
}
//different textures
public Sprite[] emptyTextures;
public Sprite dangerTexture;
// Use this for initialization
void Start()
{
//game randomly decides if square is dangerous
//10% chance for initial testing
//Chance likely to be changed at some point
dangerous = Random.value < 0.1;
//registers square in grid
int x = (int)transform.position.x;
int y = (int)transform.position.y;
Grid.elements[x, y] = this;
}
void OnMouseUpAsButton()
{
// It's dangerous
if (dangerous)
{
//uncover all mines
//calls for procedure "uncoverDanger()" from class "Grid"
Grid.uncoverDanger();
//ToDo: display loss screen
// game over
print("Game Over");
}
// It's not a mine
else
{
// change to correct texture
int x = (int)transform.position.x;
int y = (int)transform.position.y;
loadTexture(Grid.adjacentCount(x, y));
//Flood Fill nearby area
Grid.FFuncover(x, y, new bool[Grid.w, Grid.h]);
// checks if the player has won
if (Grid.gameWon() == true)
//tells the player they have won
print("you win");
}
}
//Loads a different texture
public void loadTexture(int adjacentCount)
{
//checks to see if square is dangerous
if (dangerous)
//Changes to correct texture using Unity Component
GetComponent<SpriteRenderer>().sprite = dangerTexture;
//if not dangerous
else
//changes to correct texture using Unity Component
GetComponent<SpriteRenderer>().sprite = emptyTextures[adjacentCount];
}
}
and my Grid class code is:
using System.Collections;
using UnityEngine;
public class Grid {
public static int w = 10; // w is the width of the grid (10)
public static int h = 13; // h is the height of the grid (13)
public static Square[,] elements = new Square[w, h];
//Uncover all mines when losing
public static void uncoverDanger() {
foreach (Square elem in elements)
if (elem.dangerous)
elem.loadTexture(0);
}
// Determines if the square is dangerous
public static bool dangerAt(int x, int y)
{
// Makes sure the square is in the range
if (x >= 0 && y >= 0 && x < w && y < h)
// Checks if the square is dangerous
return elements[x, y].dangerous;
return false;
}
// Counts number of dangerous squares adjacent
public static int adjacentCount(int x, int y)
{
int count = 0;
//checks adjacent squares
//potentially adds to counter
if (dangerAt(x, y + 1)) ++count; // top
if (dangerAt(x + 1, y + 1)) ++count; // top-right
if (dangerAt(x + 1, y)) ++count; // right
if (dangerAt(x + 1, y - 1)) ++count; // bottom-right
if (dangerAt(x, y - 1)) ++count; // bottom
if (dangerAt(x - 1, y - 1)) ++count; // bottom-left
if (dangerAt(x - 1, y)) ++count; // left
if (dangerAt(x - 1, y + 1)) ++count; // top-left
//outputs value
return count;
}
// Flood Fill (FF) safe squares
public static void FFuncover(int x, int y, bool[,] visited)
{
// Checks if coordinates in range
if (x >= 0 && y >= 0 && x < w && y < h)
{
// Filled already?
if (visited[x, y])
return;
//uncovers a square
elements[x, y].loadTexture(adjacentCount(x, y));
//checks if adjacencyCount is not 0
if (adjacentCount(x, y) > 0)
//ends algorithm
return;
// set square as visited
visited[x, y] = true;
// repeates with all 4 adjacent squares (not using diagonal)
FFuncover(x - 1, y, visited);
FFuncover(x + 1, y, visited);
FFuncover(x, y - 1, visited);
FFuncover(x, y + 1, visited);
}
}
public static bool gameWon()
{
// Checks all squares in the array
foreach (Square elem in elements)
// Tries to find a covered, safe square
if (elem.IsCovered() && !elem.dangerous)
// The player is not finished
return false;
// The player has won
return true;
}
}
I am a real newbie to unity so it might be something stupid, any help would be greatly appreciated.
Turns out I am an idiot, I just had to change the name of the texture in the variable code to "Uncleared proposed", I don't know why it didn't work earlier.
I am trying to copy the Zelda health system. The code looks really fine and works fine.
But the heart containers are placed wrong. They get instantiated below the canvas.
This is the important code, the heart containers are correct, just at the wrong position.
The calculation of x and y is correct, but on the canvas it is not.
private Transform healthBar = GameObject.FindGameObjectWithTag("HealthController").transform; // container for the heartContainers
private GameObject healthWrapperObject = Resources.Load("HealthContainer") as GameObject; // the backgroundImage and parent of the heart
private List<Image> healthContainers = new List<Image>(); // list of hearts for later usages
private int maxHealth = 6;
private int currentHealth;
private int healthPerHealthContainer = 4; // 4 lifepoints per heart
private int healthContainersPerRow = 5; // 5 hearts per row
private int healthContainerStartPositionX = 0; // Healthbar starts at 0 on x
private int healthContainerStartPositionY = 0; // Healthbar starts at 0 on y
private int healthContainerSpacingX = 10; // horizontal spacing
private int healthContainerSpacingY = -10; // vertical spacing
private void Start()
{
currentHealth = maxHealth;
InitializeHealthBar();
}
public void InitializeHealthBar()
{
int neededHealthContainers = maxHealth % healthPerHealthContainer == 0 ? maxHealth / healthPerHealthContainer : maxHealth / healthPerHealthContainer + 1; // Calculate the needed container count
int counter = 0; // counts the hearts per row
int x = healthContainerStartPositionX; // horizontal position of the heartContainer
int y = healthContainerStartPositionY; // vertical position of the heartContainer
for (int i = 0; i < neededHealthContainers; i++)
{
counter++;
if (counter >= healthContainersPerRow) // start a new line after 5 hearts per row
{
x = healthContainerStartPositionX; // move back to the left
y += healthContainerSpacingY; // go for the next line
counter = 0; // reset the counter
}
else
x += healthContainerSpacingX; // place the new container right next to the previous
Transform newHealthContainerTransform = Instantiate(healthWrapperObject, new Vector2(x, y), healthWrapperObject.transform.rotation).transform; // create the healthContainer parent / backgroundImage
newHealthContainerTransform.SetParent(healthBar); // take the container and make it a child of the healthBar
healthContainers.Add(newHealthContainerTransform.GetChild(0).GetComponent<Image>()); // get the heart of the heartContainer and add it to the heartList
}
}
I added the transform settings for the healthBar, the healthContainer / backgroundImage and the heart ("healthfill").
On all 3 elements I pressed Strg+Alt and Shift for anchoring them.
The heartcontainter should be added to the healthbar, the heart is a child of the heartcontainer and is set to stretch (it should be the same size as its parent)
Why are the UI prefab Objects instantiated below the canvas?
I assume you are getting something like this:
You fix this by pass false to the second parameter of the SetParent function. By doing this, you will make the Transform keep its local orientation rather than its global orientation.
Simply replace :
newHealthContainerTransform.SetParent(healthBar);
with:
newHealthContainerTransform.SetParent(healthBar, false)
You can also set the parent Object and make the instantiated Object's Transform keep its local orientation in the Instantiate function. The only disadvantage of this is that you now have to set the position of object in another line of code instead of the Instantiate function like before.
Transform newHealthContainerTransform = Instantiate(healthWrapperObject, healthBar, false).transform;
newHealthContainerTransform.GetComponent<RectTransform>().anchoredPosition3D = new Vector2(x, y);
When moving a UI Object you should be modifying it's RectTransform variables instead of the Transform variables.
Below are other useful variables that determines where to position the UI:
These are anchoredPosition, anchoredPosition3D, anchorMax and anchorMin which can be modified with:
yourUIObj.GetComponent<RectTransform>().anchoredPosition = ...
yourUIObj.GetComponent<RectTransform>().anchoredPosition3D = ...
yourUIObj.GetComponent<RectTransform>().anchorMax = ...
yourUIObj.GetComponent<RectTransform>().anchorMin = ...
I have a 2D Array of Objects that move synchronously on the screen and I need to detect when they hit the edge of the screen so that they change direction (think Space Invaders). So far, I had it working with a Rectangle pre-defined to be the size and position of the objects in the Array but the Objects can be hit with missiles and are no longer drawn so when all of them on one side are destroyed, the rectangle stays the same size and they change direction too early.
Is there a better way to do what I want to do? This is my code for the functionality at the moment:
(In LoadContent Method)
invaderRect = new Rectangle(
0, 0,
invadersWide * (invaderImage.Width + 15) - 15,
invadersHigh * (invaderImage.Height + 15) - 15);
(In Update Method)
if ((invaderRect.X + invaderRect.Width) >= screenRectangle.Width - 15)
invadersHitWall = true;
else if (invaderRect.X <= 15)
invadersHitWall = false;
if (!invadersHitWall)
invaderRect.X += 2;
else if (invadersHitWall)
invaderRect.X -= 2;
It's all a matter of organizing your code. You should use an object oriented approach. Create a class that represents your game objects. I would use an abstract base class defining the basic properties and methods. Then derive concrete game objects from this base class. The concrete game objects' constructors have the task to initialize the game objects.
public abstract class GameObject
{
protected BlockType[,] _buildingBlocks; // The 2-d array. Replace "BlockType"
// by the type you are using.
protected int _x0, _y0; // Indexes of the first non empty block.
protected int _x1, _y1; // Indexes of the last non empty block + 1.
// Pixel coordinates of upper left corner of the intact game object.
public Point Location { get; set; }
// Represents the current position and size of the possibly diminished
// game object in pixels.
public Rectangle BoundingBox
{
get {
return new Rectangle(
Location.X + BlockSize * _x0,
Location.Y + BlockSize * _y0,
BlockSize * (_x1 - _x0),
BlockSize * (_y1 - _y0)
);
}
}
public void Draw(Graphics g, int x, int x)
{
for (int i = _x0; i < _x1; i++) {
for (int j = _y0; j < _y1; j++) {
BlockType block = _buildingBlocks[i, j];
if (block != null) {
// Replace by the appropriate drawing methods for XNA.
g.FillRectangle(block.Brush,
x + BlockSize * i, y + BlockSize * j,
BlockSize, BlockSize);
}
}
}
}
// Call this after changes have been made to the arrray which may affect the
// apparent size of the game object, e.g. after the object was hit by a bomb.
protected void CalculateBounds()
{
_x0 = _buildingBlocks.GetLength(0);
_y0 = _buildingBlocks.GetLength(1);
_x1 = 0;
_y1 = 0;
for (int i = 0; i < _buildingBlocks.GetLength(0); i++) {
for (int j = 0; j < _buildingBlocks.GetLength(1); j++) {
if (buildingBlocks[i, j] != null) {
_x0 = Math.Min(_x0, i);
_y0 = Math.Min(_y0, j);
_x1 = Math.Max(_x1, i + 1);
_y1 = Math.Max(_y1, j + 1);
}
}
}
}
public void DestroyBlocksAt(IEnumerable<Point> points)
{
//TODO: destroy hit blocks.
CalculateBounds();
}
}
After an object has been hit by a bomb, call CalculateBounds(); in order to recalculate the real size of the object. The idea is that instead of using invaderRect you would be using the BoundingBox property that reflects the real extents of the gaming object. BoundingBox takes into account the position of the game object on the screen (Location) and the position within the array (_x0, _x1, _y0, _y1).
This is a raw sketch. You may have to refine it and adapt it to your current logic. Don't use magical numbers like 15. Define constants like public const int BlockSize = 15;. This makes it easier to change the numbers later and also to understand the code.
Here is an example of an invader class
public class Invader : GameObject
{
private const int WIDTH = 10, HEIGHT = 7; // Width and height of invader in blocks.
public Invader()
{
_buildingBlocks = new BlockType[WIDTH, HEIGHT];
_x1 = WIDTH;
_y1 = HEIGHT;
_buildingBlocks[0, 0] = ...
...
}
}
UPDATE
As I understood your post, every movable “thing” is stored in its own 2D array storing the 15 x 15 pixel blocks it is made of. GameObject is the base class of all the visible movable things, like invaders, space ships, bombs and so on and is a wrapper around the 2D arrays (named _buildingBlocks in my code examples). It adds the logic needed to determine the real bounds of objects after they have been hit by bombs. CalculateBounds recalculates the position and size of the remaining object after a bomb hit within the 2D array (of course you must call it every time the shape of the objects changes). BoundingBox moves these internal 2D array bounds (stored in _x0, _x1, _y0 and _y1) to real screen positions (stored in the Location property) by multiplying with the block size (the 15 pixels) and adding the screen location.
For every game object type (i.e. movable shape type) you have to derive a class (e.g. a class Invader for invaders). Then create an invader object with Invader invader = new Invader(); for every single invader. GameObject is not the main class. The main class includes the game loop and game logic. It creates game objects and calculates their new positions (stored in the Location property) as they move around. Instead of working with invaderRect the wall-hitting logic would now work with BoundingBox which returns the real size and positions of the objects.
Rectangle invaderBounds = invader.BoundingBox;
bool isLeftWallHit = invaderBounds.Left <= 0;
bool isRightWallHit = invaderBounds.Right >= screenRectangle.Width;
bool isUpperWallHit = invaderBounds.Top <= 0;
bool isLowerWallHit = invaderBounds.Bottom >= screenRectangle.Height;