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 = ...
Related
Very new developer here.
In my program I have a randomly generating world map using a simplex noise library. On top of this I am attempting to draw a tilemap of transparent 4x4 tiles that appear slightly translucent when the mouse is hovering over one.
I've got this working but it takes about 3 whole seconds for the highlighted tile to update to the mouse's current position. Is there anything I could do to solve this?
This is my code for the MouseState check in the tile class:
public override void Update(GameTime gameTime)
{
_previousMouse = _currentMouse;
_currentMouse = Mouse.GetState();
var mouseRectangle = new Rectangle(_currentMouse.X, _currentMouse.Y, 1, 1);
_isHovering = false;
if (mouseRectangle.Intersects(Rectangle))
{
_isHovering = true;
if (_currentMouse.LeftButton == ButtonState.Released && _previousMouse.LeftButton == ButtonState.Pressed)
{
Click?.Invoke(this, new EventArgs());
}
}
}
Sorry if this is formatted wrong or badly asked, first post so still getting to grips with everything :)
Invert the logic:
Instead of checking thousands of tile objects against the mouse, apply the mouse to a single object.
Assuming you have a list or array of tile objects:
Add a new object to check for mouse hover and click:
public class MouseDetect(Tile[] tiles) // replace with List<> as needed
{
int PrevHover = -1; // used to unHover
// if (Area == Screen) make the next two lines `const`, so the compiler will remove all uses...
int AreaX = 0; //Area x offset
int AreaY = 0; //Area y offset
int AreaW = 800; //Area width
int AreaH = 480; //Area height
const int Grid = 4; // assumes square
const int GridW = AreaW / Grid;
// I Will assume the `Delegate Click` in `Tile` is public
public void Update(MouseState ms, MouseState oms), //_currentMouse = ms and _previousMouse = oms;
{
int mouseIndex = (ms.X - AreaX) % Gridw + (ms.Y - AreaY) / GridW;
tiles[PrevHover].Hover = false;
PrevHover = mouseIndex;
tiles[PrevHover].Hover = true;
//Check Release
if(tiles[PrevHover].Hover && ms.LeftButton == ms.ButtonState.Released && oms.LeftButton == ButtonState.Pressed)
tiles[PrevHover].Click(tiles[PrevHover], new EventArgs());
}
}
Remove the Update from the Tile class.
Notes for anyone reading this later:
Never call Mouse.GetState(); more than once per step.
Predefined or framework names such as Rectangle should never be used as an identifier.
i.e renamed and corrected to CollRectangle
if (CollRectangle.Contains(ms.Position))
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.
I want to create a grid with sprites in Unity. Each cell should have a number on it.
This is how it should look
and my grid looks this
So I generate the cells and add them to a empty gameobject called
Map
private GameObject cellPrefab;
private const int CELL_COUNT_X = 10; // create 100 cells
private const int CELL_COUNT_Y = 10;
private const float CELL_SPACING = 1.1f; // with a small spacing
private List<Cell> cells = new List<Cell>(); // store all cells here
private const int NUM_RANGE_MIN = 1; // cell value range
private const int NUM_RANGE_MAX = 10;
private void Start()
{
cellPrefab = Resources.Load(StringCollection.CELL) as GameObject;
for (int x = 0; x < CELL_COUNT_X; x++)
{
for (int y = 0; y < CELL_COUNT_Y; y++)
{
float spawnPosX = x * CELL_SPACING - CELL_COUNT_X / 2;
float spawnPosY = y * CELL_SPACING - CELL_COUNT_Y / 2;
GameObject cell = Instantiate(cellPrefab, new Vector2(spawnPosX, spawnPosY), cellPrefab.transform.rotation); // create the new cell
cell.transform.SetParent(transform); // add the cell to the map
Cell cellComponent = cell.GetComponent<Cell>();
cellComponent.InitCell(Random.Range(NUM_RANGE_MIN, NUM_RANGE_MAX)); // init the cell value
cells.Add(cellComponent); // add to list
}
}
}
Each cell got this script attached
private Text txtCellValue;
private int cellValue;
public void InitCell(int value)
{
txtCellValue = transform.GetChild(0).GetChild(0).GetComponent<Text>(); // get the text component of the cell
cellValue = value; // set the value
txtCellValue.text = cellValue.ToString(); // update the GUI
}
So in the hierarchy, each cell is added to the "Map" and got this own hierarchy
The canvas is set on "Scale with Screensize" and the text itself has these settings
I just want to write the value of the cell on this sprite. Maybe there is a more clean way?
Would be nice if someone could help fixing this!
You will select render mode "world" for your canvas. Then set the scale and width/height values.
Also, you will remember about sorting layers. Canvas layer would be bigger than sprite renderer, if you dont use separate camera for UI.
Below is a few simple lines of code that is part of uni tech demo. In an attempt to create a 3D grid of vectors within a given area.
My solution thus far is to create a 2D grid at the starting X and Y points and then repeat this process along the Z.
As a temporary visualization I then instantiate Sphere prefabs.
The purpose of which is to use this grid of vectors as a model for a depth first search path algorithm which I will use to input vectors for a procedurally generated track(currently control points are set manually via editor methods)
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class GridLayout : MonoBehaviour {
public int GridWidth;
public int GridLength;
public int GridHeight;
public int resolution;
private int ResW;
private int ResL;
private int ResH;
private List<Vector3> GridPoints = new List<Vector3>();
private bool GridCompleted = false;
private GameObject tempObject;
// Use this for initialization
void Start () {
//Area box Start square
GridPoints.Add(new Vector3(0,0,0));
GridPoints.Add(new Vector3(0,GridHeight,0));
GridPoints.Add(new Vector3(GridWidth,0,0));
GridPoints.Add(new Vector3(GridWidth,GridHeight,0));
ResW = GridWidth/resolution;
ResH = GridHeight/resolution;
ResL = GridLength/resolution;
}
// Update is called once per frame
void Update () {
if (GridCompleted == false)
CreateGrid();
else
{
for(int i = 0; i <= GridPoints.Count; i++)
{
tempObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
tempObject.transform.position = GridPoints[i];
}
}
} //Area Box End square
// GridPoints.Add(new Vector3(0,0,GridLength));
// GridPoints.Add(new Vector3(0,GridHeight,GridLength));
// GridPoints.Add(new Vector3(GridWidth,0,GridLength));
// GridPoints.Add(new Vector3(GridWidth,GridHeight,GridLength));
void CreateGrid()
{
if(ResW != GridWidth | ResL != GridLength | ResH != GridHeight)
{
for(int l = 1;ResL <= GridLength; l++)
{
GridPoints.Add (new Vector3(0,0,ResL));
ResL = (GridLength/(resolution))*l;
for(int w = 1;ResW <= GridWidth; w++)
{
GridPoints.Add (new Vector3(ResW,0,0));
ResW = (GridWidth/(resolution))*w;
for(int h = 1;ResW <= GridHeight; h++)
{
GridPoints.Add (new Vector3(0,ResH,0));
ResH = (GridHeight/(resolution))*h;
}
}
}
}
else
{
GridCompleted = true;
}
}
}
Unfortunately this triple for loop leads to a memory exception - this is a high end PC however I will be forced to run my project on a laptop with 4GB ram.
With that in mind : is there a more memory efficient way of creating a vector3 grid.
Thanks in advance.
Typing failure in line
for(int h = 1;ResW <= GridHeight; h++)
Is causing your memory problems. The most inner loop runs infinitely, it should be ResH. Therefore I suggest to check the for-variable inside the for-statement and not something else:
for(int h = 1; h < wharever; h++)
secondly: bad code formatting and indentation.
Finally, so far, a list<object> is a 1D structure. A 3rd structure would be a list<list<list<object>>> or a array object [][][]:
Vector3 [][][] vector3grid;
vector3grid = new vector3[lenX][][];
for (int x=0; x<lenX; x++)
{
vector3grid[x] = new vector3 [lenY][];
for (int y=0; y<lenY; y++)
{
vector3grid[x][y] = new vector3 [lenZ];
// init if needed:
for(int z=0; ...
vector3grid[x][y][z] = ...
}
}
Edit:
I just noticed that my answer is not 100% correct. The sample above is 1 of 2 (or more) ways to create a 3D array. The easier one of them is following:
In C++/cli:
Array<vector3, 3>^ vector3grid = gcnew array<vector3, 3>(lenX, lenY, lenZ);
For c# and VB.net I need to look up the Syntax first.
This is a REAL 3D array now. ;-)
Edit 2:
3D in c#:
Vector3 [,,] vector3grid = New vector3[lenX,lenY,lenZ];
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;