Is there a way to create a "honeycomb" looking health UI using Layout Components?

,

I am currently creating a health/stamina system in my Unity 2D game project, and I am trying to build a custom layout using the built-in Grid/Horizontal/Vertical Layout Group components. The layout I am aiming to achieve will look similar to this image:

image

Currently, the layout I have implemented has the (heart/crystal) prefab being instantiated horizontally one after another under the “HeartHolder” gameobject, as you can see in the very last image in this post. I am currently only using a “Grid Layout Group” component on a UI Image object (HeartHolder). Some snips of these GameObjects and their Components are provided below. If you need to see the script(s) I can provide those as well, but there’s really nothing special going on in them. Basically, the prefabs get instantiated, and the HeartHolder gameobject is assigned as their parent transform.

image

Instead of instantiating the prefabs side by side (seen in the photo below), I want to add them diagonally from the last instantiated “heart” (like the photo above). Really I’m mostly lost on where to start looking for information regarding this issue, as my previous searches haven’t yielded the results I want.

Any links or keywords you can provide to get my search started on this topic would be greatly appreciated!

image

I have thus far tried using multiple different search terms/keywords to look for solutions already given to this problem. The last phrase (… honeycomb ui layout) returned some tutorials on how to create levels with a hexagonal grid, but they dont use UI layout components (obviously) they use grid components. Is there a way to achieve the effect I want with Layout Components?

Terms Used:

  • how to indent a row in a grid layout unity 2d
  • change x-axis offset of Grid Layout row in unity
  • unity 2d how to create honeycomb ui layout

I have not tried this solution yet, but I would assume if I put an empty child object under HeartHolder with a horizontal layout group, I could increase the padding on the left side of the object, and then alternate between the two objects when instantiating the heart prefabs. This would achieve the illusion of having the two rows offset horizontally, but that doesn’t feel like the most efficient way to achieve such an effect in a powerful game engine like Unity.

IMO , you’re looking too deep into it , do you even need a LayoutGroup for this ? just have the script that instantiates the “Health Points” take care of placing them directly in the correct position , no need for any kinds of layouts.
Personally i would look at positionning the UIs based on their parent’s position AND based on whether the element’s index is odd or even , here’s an example for placing 4 health points
image

Here’s some pseudo code of how i would do it.

// Call this method to create the Healthbar correctly based on the "hpCount" passed
public void RefreshHealthPoints(RectTransform uiPrefab , RectTransform uiParent , int hpCount)
{      
        Vector2 step = new Vector2(uiPrefab.rect.width, uiPrefab.rect.height) * 0.5f;

        for(int i = 0; i < hpCount; ++i)
        {
              Vector2 pos;
              pos.x = i * step.x;

              if( i % 2 == 1)
                   pos.y = -step.y
              
              RectTransform spawnedUi = Instantiate(uiPrefab , uiParent);
              spawnedUi.anchoredPosition = pos;
}

Hope this helps

The above comment was definitely very helpful. I tend to overcomplicate things a lot.

My solution wasn’t exactly this, but it was pretty close, I will make a short YouTube tutorial on this soon so that people can see how I actually achieved the full solution. Because now, not only do the hearts instantiate in a honeycomb style, but they (can) also get decreased by half a point at a time instead of a full heart, so there’s a couple of things that not a lot of tutorials currently cover.

This definitely isn’t the most efficient way to achieve this effect because you have to manually set the vert/horiz spacing, but for some reason, I couldn’t properly gather the width/height of the prefab, so this’ll do.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class HeartManager : MonoBehaviour
{
    [SerializeField] private GameObject heartPrefab;
    [SerializeField] private PlayerHealth playerHealth;
    private List<HeartState> hearts = new List<HeartState>();
    [SerializeField] private float xSpacing; // Adjust this value as needed for xSpacing between hearts
    [SerializeField] private float ySpacing; // Adjust this value as needed for spacing between hearts

    private void OnEnable()
    {
        PlayerHealth.OnPlayerDamage += DrawHearts;
    }

    private void OnDisable()
    {
        PlayerHealth.OnPlayerDamage -= DrawHearts;
    }

    private void Start()
    {
        DrawHearts();
    }

    public void DrawHearts()
    {
        ClearHearts();

        // determine total hearts to make based on max health
        float maxHealthRemainder = playerHealth.maxHealth % 2;
        int heartsToMake = (int)(playerHealth.maxHealth / 2 + maxHealthRemainder);

        for (int i = 0; i < heartsToMake; i++) 
        {
            CreateEmptyHeart();
        }

        for (int i = 0; i < hearts.Count; i++)
        {
            // determine heart status
            int heartStatusRemainder = (int)Mathf.Clamp(playerHealth.health - (i * 2), 0, 2);
            hearts[i].SetHeartImage((HeartStatus)heartStatusRemainder);
        }

    }

    public void CreateEmptyHeart()
    {
        GameObject newHeart = Instantiate(heartPrefab);
        newHeart.transform.SetParent(gameObject.transform);
        int heartIndex = hearts.Count;
        float heartWidth = newHeart.GetComponent<RectTransform>().rect.width;
        float heartHeight = newHeart.GetComponent<RectTransform>().rect.height;

        if (heartIndex == 0)
        {
            newHeart.transform.localPosition = Vector3.zero;
        }
        else // Adjust the position based on the previously spawned heart
        {
            Vector2 prevHeartPos = hearts[heartIndex - 1].transform.GetComponent<RectTransform>().anchoredPosition;
             
            Vector2 pos = Vector2.zero;

            if (heartIndex % 2 == 1)
            {
                pos = new Vector2(prevHeartPos.x + xSpacing, prevHeartPos.y + ySpacing);
            } 
            else
            {
                pos = new Vector2(prevHeartPos.x + xSpacing, prevHeartPos.y  - ySpacing);
            }

            newHeart.GetComponent<RectTransform>().anchoredPosition = pos;
        }

        newHeart.transform.localScale = new Vector3(62,62,0);

        HeartState newHeartState = newHeart.GetComponent<HeartState>();
        newHeartState.SetHeartImage(HeartStatus.Empty);
        hearts.Add(newHeartState);
    }

    public void ClearHearts()
    {
        foreach (Transform t in transform)
        {
            Destroy(t.gameObject);
        }
        hearts = new List<HeartState>();
    }

(Image of Results)