How to make custom Unity LayoutGroup expand to fit contents - c#

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;
}
}

Related

How do I convert between enums for passing between different functions

I have enums set up in a script called Grid, as follows:
public enum CellType
{
Empty,
Road,
Structure,
SpecialStructure,
None
}
Then I have a script called placement manager which adds data relevant to that enum like this:
internal void PlaceObjectOnTheMap(Vector3Int position, GameObject structurePrefab, CellType type, int width = 1, int height = 1)
{
StructureModel structure = CreateANewStructureModel(position, structurePrefab, type);
var structureNeedingRoad = structure.GetComponent<INeedingRoad>();
if (structureNeedingRoad != null)
{
structureNeedingRoad.RoadPosition = GetNearestRoad(position, width, height).Value;
Debug.Log("My nearest road position is: " + structureNeedingRoad.RoadPosition);
}
for (int x = 0; x < width; x++)
{
for (int z = 0; z < height; z++)
{
var newPosition = position + new Vector3Int(x, 0, z);
placementGrid[newPosition.x, newPosition.z] = type;
structureDictionary.Add(newPosition, structure);
DestroyNatureAt(newPosition);
}
}
}
finally, I have another script called Grid Helper which is supposed to call placement manager and add itself to the grid which I've set like this:
public enum CellType
{
Empty,
Road,
Structure,
SpecialStructure,
None
}
[SerializeField]
private CellType structureType = CellType.Empty;
public PlacementManager placementManager;
// Start is called before the first frame update
void Start()
{
placementManager.PlaceObjectOnTheMap(new Vector3Int(Mathf.FloorToInt(transform.position.x),
Mathf.FloorToInt(transform.position.y),
Mathf.FloorToInt(transform.position.z)), this.gameObject, structureType);
}
but I somehow keep getting told Argument3 cannot convert from GridHelper.CellType to CellType.
What am I doing wrong?
Remove your enum in the GridHelper or in the Grid class and use GridHelper.CellType or Grid.CellType in the other class depending on what you removed.

Slow update time with tiles and MouseState MonoGame

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))

Ui button doesn't move correctly

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.

Unity, how to change only "top" of GameObject rect transform

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);

2D parallaxing background on both X and Y axis using XNA and C#

I am working on a space shooter game using XNA and have followed multiple tutorials to create a parallax background. So far I can get it to go along one axis, either X or Y, but not both at the same time. I have a camera class that follows the player, which the player moves (instead of the 'world'), since I figured it would be easier to just move the player versus moving everything else around the player.
So far, the background doesn't keep up with the player, and it also can't comprehend both axis at the same time. I thought about a tile engine, but that wouldn't let me parallax different layers would it?
Could anyone help me understand what I need to do, or recommend a tutorial that can do both axis at the same time? I can't seem to find the answer on my own this time.
Here is the code for my background class:
class Background
{
// Textures to hold the two background images
Texture2D spaceBackground, starsParallax;
int backgroundWidth = 2048;
int backgroundHeight = 2048;
int parallaxWidth = 2048;
int parallaxHeight = 2048;
int backgroundWidthOffset;
int backgroundHeightOffset;
int parallaxWidthOffset;
int parallaxHeightOffset;
public int BackgroundWidthOffset
{
get { return backgroundWidthOffset; }
set
{
backgroundWidthOffset = value;
if (backgroundWidthOffset < 0)
{
backgroundWidthOffset += backgroundWidth;
}
if (backgroundWidthOffset > backgroundWidth)
{
backgroundWidthOffset -= backgroundWidth;
}
}
}
public int BackgroundHeightOffset
{
get { return backgroundHeightOffset; }
set
{
backgroundHeightOffset = value;
if (backgroundHeightOffset < 0)
{
backgroundHeightOffset += backgroundHeight;
}
if (backgroundHeightOffset > backgroundHeight)
{
backgroundHeightOffset -= backgroundHeight;
}
}
}
public int ParallaxWidthOffset
{
get { return parallaxWidthOffset; }
set
{
parallaxWidthOffset = value;
if (parallaxWidthOffset < 0)
{
parallaxWidthOffset += parallaxWidth;
}
if (parallaxWidthOffset > parallaxWidth)
{
parallaxWidthOffset -= parallaxWidth;
}
}
}
public int ParallaxHeightOffset
{
get { return parallaxHeightOffset; }
set
{
parallaxHeightOffset = value;
if (parallaxHeightOffset < 0)
{
parallaxHeightOffset += parallaxHeight;
}
if (parallaxHeightOffset > parallaxHeight)
{
parallaxHeightOffset -= parallaxHeight;
}
}
}
// Constructor when passed a Content Manager and two strings
public Background(ContentManager content,
string sBackground, string sParallax)
{
spaceBackground = content.Load<Texture2D>(sBackground);
backgroundWidth = spaceBackground.Width;
backgroundHeight = spaceBackground.Height;
starsParallax = content.Load<Texture2D>(sParallax);
parallaxWidth = starsParallax.Width;
parallaxHeight = starsParallax.Height;
}
public void Draw(SpriteBatch spriteBatch)
{
// Draw the background panel, offset by the player's location
spriteBatch.Draw(
spaceBackground,
new Rectangle(-1 * backgroundWidthOffset,
-1 * backgroundHeightOffset,
backgroundWidth,
backgroundHeight),
Color.White);
// If the right edge of the background panel will end
// within the bounds of the display, draw a second copy
// of the background at that location.
if (backgroundWidthOffset > backgroundWidth)
{
spriteBatch.Draw(
spaceBackground,
new Rectangle(
(-1 * backgroundWidthOffset) + backgroundWidth, 0,
backgroundWidth, backgroundHeight),
Color.White);
}
else //(backgroundHeightOffset > backgroundHeight - viewportHeight)
{
spriteBatch.Draw(
spaceBackground,
new Rectangle(
0, (-1 * backgroundHeightOffset) + backgroundHeight,
backgroundHeight, backgroundHeight),
Color.White);
}
// Draw the parallax star field
spriteBatch.Draw(
starsParallax,
new Rectangle(-1 * parallaxWidthOffset,
0, parallaxWidth,
parallaxHeight),
Color.SlateGray);
// if the player is past the point where the star
// field will end on the active screen we need
// to draw a second copy of it to cover the
// remaining screen area.
if (parallaxWidthOffset > parallaxWidth)
{
spriteBatch.Draw(
starsParallax,
new Rectangle(
(-1 * parallaxWidthOffset) + parallaxWidth,
0,
parallaxWidth,
parallaxHeight),
Color.White);
}
}
}
The main game then has ties to move the backgrounds, along with the player.
background.BackgroundWidthOffset -= 2;
background.ParallaxWidthOffset -= 1;
Visually, the background is sort of jumpy, and seems to randomly skip or overlap background tiles.
I've used this method in the past with great results:
http://www.david-gouveia.com/scrolling-textures-with-zoom-and-rotation/
It uses a shader to accomplish the effect, resulting in a fast implementation.
There is a complete example here.

Categories