Hobby projects

Autochess

An autobattler prototype based on Dota 2’s autochess mod.

https://github.com/CasLuitwieler/AutoChess

public class NewShop : MonoBehaviour
{
    [SerializeField] private Hero[] availableHeroes = null;

    [SerializeField] private ShopHeroButton[] shopHeroButtons = null;

    [SerializeField] private BoardManager boardManager = null;

    [SerializeField] private GameObject heroPrefab = null;
    [SerializeField] private GameObject heroContainer = null;

    [SerializeField] private Player player = null;

	private Hero[] heroRotation = new Hero[5];
    private Inventory inventory = null;

    private int rerollPrice = 2;

    private void Start()
    {
        inventory = player.Inventory;

        SetRandomHeroes();
    }

    public void SetRandomHeroes()
    {
        int amountOfHeroes = availableHeroes.Length;
        
        for (int i = 0; i < shopHeroButtons.Length; i++)
        {
            Hero randomHero = availableHeroes[Random.Range(0, amountOfHeroes - 1)];
            shopHeroButtons[i].SetHero(randomHero);
            heroRotation[i] = randomHero;
        }
    }

    public void BuyHero(int heroID)
    {
        //check if the shop hero hasn't been purchased already
        if (heroRotation[heroID] == null)
            return;
        Hero purchasedHero = heroRotation[heroID];

        //if the player doesn't have enough gold, return
        if (inventory.Gold < purchasedHero.Price)
        {
            Debug.Log("Not enought gold to purchase hero");
            return;
        }

        inventory.SubtractGold(purchasedHero.Price);

        //if the bench is full, return
        if (!boardManager.AvailableSpotOnBench())
            return;
        
        //instantiate hero object
        GameObject newHero = Instantiate(heroPrefab, heroContainer.transform);
        //set the hero properties
        HeroProperties heroProperties = newHero.GetComponent<HeroProperties>();
        heroProperties.SetupHero(purchasedHero);
        //add hero to the benchHeroes
        boardManager.PlaceOnBench(newHero);

        //make sure the same hero can't be purchased multiple timnes
        heroRotation[heroID] = null;
    }

    public void Reroll()
    {
        if (inventory.Gold < rerollPrice)
            return;

        inventory.SubtractGold(rerollPrice);

        SetRandomHeroes();
    }
}
public class Inventory
{
    public int Gold { get; private set; }
    public bool HasChanged { get; set; }

    public List<GameObject> benchHeroes { get; private set; }
    public List<GameObject> boardHeroes { get; private set; }

    public int amountOfBenchHeroes { get; private set; }
    public int amountOfBoardHeroes { get; private set; }

    public Inventory(List<GameObject> benchHeroes, List<GameObject> boardHeroes)
    {
        this.benchHeroes = benchHeroes;
        this.boardHeroes = boardHeroes;
    }

    public bool PlaceHeroOnBoard(GameObject hero)
    {
        bool removedSuccesfully;
        removedSuccesfully = benchHeroes.Remove(hero);

        if (!removedSuccesfully)
            return false;
        else
            boardHeroes.Add(hero);

        amountOfBenchHeroes--;
        HasChanged = true;
        return true;
    }

    public bool RetrieveHeroToBench(GameObject hero)
    {
        bool removedSuccesfully;
        removedSuccesfully = boardHeroes.Remove(hero);

        if (!removedSuccesfully)
            return false;
        else
            benchHeroes.Add(hero);

        amountOfBenchHeroes++;
        HasChanged = true;
        return true;
    }

    public bool RemoveHero(GameObject hero)
    {
        Hero heroProperties = hero.GetComponent<HeroProperties>().Hero;
        HasChanged = true;
        if (heroProperties.OnBoard)
            return boardHeroes.Remove(hero);
        else
        {
            amountOfBenchHeroes--;
            return benchHeroes.Remove(hero);
        }
            
    }

    public void AddGold(int amount)
    {
        Gold += amount;
    }

    public void SubtractGold(int amount)
    {
        Gold -= amount;
    }
}
public class BoardManager : MonoBehaviour
{
    public Tile[] BenchTiles = new Tile[8];
    public Tile[] BoardTiles = new Tile[64];
    
    private List<GameObject> heroes = new List<GameObject>();

    private GameObject[] benchHeroes = new GameObject[8];
    private GameObject[] boardHeroes = new GameObject[10];

    private GameObject[] currentArray, targetArray;

    private void Awake()
    {
        //set tile numbers
        for (int i = 0; i < BenchTiles.Length; i++)
            BenchTiles[i].TileNumber = i;
        for (int i = 0; i < BoardTiles.Length; i++)
            BoardTiles[i].TileNumber = i;
    }

    public void PlaceOnBench(GameObject hero)
    {
        HeroController heroController = hero.GetComponent<HeroController>();

        //remove from board if the hero was on the board
        if(heroController.OnBoard)
        {
            if (CheckForEntry(boardHeroes, out int oldEntry, hero))
                boardHeroes[oldEntry] = null;
        }

        //add the hero to the bench at the first empty entry
        if (CheckForEntry(benchHeroes, out int targetIndex, null))
            benchHeroes[targetIndex] = hero;

        //set the hero position to the tile it has moved to
        if(BenchTiles.Length > 0)
            hero.transform.position = BenchTiles[targetIndex].spawnPosition;

        //store current tile id
        heroController.OnTile(targetIndex);
    }

    public bool AvailableSpotOnBench()
    {
        return CheckForEntry(benchHeroes, out int temp, null);
    }

    public void MoveHero(GameObject heroToMove, Tile targetTile)
    {
        Move moveType = CalculateMoveType(heroToMove, targetTile);
        SetCurrentAndTargetArray(moveType);
        
        if(ValidSwap(heroToMove, out int oldIndex, out int newIndex))
            SwapHero(heroToMove, oldIndex, newIndex);

        PlaceHero(heroToMove, targetTile);
    }

    private bool ValidSwap(GameObject heroToMove, out int oldIndex, out int newIndex)
    {
        oldIndex = newIndex = -1;
        //find the heroToMove in the board array
        if (CheckForEntry(currentArray, out oldIndex, heroToMove))
        {
            //find the heroToMove in the board array
            if (CheckForEntry(currentArray, out newIndex, heroToMove))
            {
                return true;
            }
        }
        return false;
    }
    
    private void SwapHero(GameObject heroToMove, int oldIndex, int newIndex)
    {
        //remove the hero from the board
        currentArray[oldIndex] = null;
        //add the hero to the bench
        targetArray[newIndex] = heroToMove;
    }

    private void PlaceHero(GameObject heroToMove, Tile targetTile)
    {
        int targetTileIndex;

        HeroController heroController = heroToMove.GetComponent<HeroController>();
        if (targetTile.isBenchTile)
        {
            targetTileIndex = Array.IndexOf(BenchTiles, targetTile);
            heroController.OnBoard = false;
        }
        else
        {
            targetTileIndex = Array.IndexOf(BoardTiles, targetTile);
            heroController.OnBoard = true;
        }

        //store current tile id
        heroController.OnTile(targetTileIndex);
        //move player to targetTile position
        heroToMove.transform.position = targetTile.spawnPosition;
    }

    private Move CalculateMoveType(GameObject hero, Tile targetTile)
    {
        HeroController heroController = hero.GetComponent<HeroController>();

        if (heroController.OnBoard && !targetTile.isBenchTile)
            return Move.BoardToBoard;
        else if (!heroController.OnBoard && targetTile.isBenchTile)
            return Move.BenchToBench;
        else if (heroController.OnBoard && targetTile.isBenchTile)
            return Move.BoardToBench;
        else if (!heroController.OnBoard && !targetTile.isBenchTile)
            return Move.BenchToBoard;
        else
            return Move.None;
    }

    private void SetCurrentAndTargetArray(Move moveType)
    {
        switch (moveType)
        {
            case Move.BenchToBench:
                currentArray = benchHeroes;
                targetArray = benchHeroes;
                break;
            case Move.BenchToBoard:
                currentArray = benchHeroes;
                targetArray = boardHeroes;
                break;
            case Move.BoardToBench:
                currentArray = boardHeroes;
                targetArray = benchHeroes;
                break;
            case Move.BoardToBoard:
                currentArray = boardHeroes;
                targetArray = boardHeroes;
                break;
            default:
                Debug.LogError("Invalid hero moveType");
                break;
        }
    }

    private bool CheckForEntry(GameObject[] heroArray, out int index, GameObject heroToFind)
    {
        for (int i = 0; i < heroArray.Length; i++)
        {
            if (heroArray[i] == heroToFind)
            {
                index = i;
                return true;
            }
        }
        index = -1;
        return false;
    }

    public GameObject[] GetBoardHeroes()
    {
        return boardHeroes;
    }

    public List<Tile> GetBoardTiles(List<int> selectedTile)
    {
        List<Tile> tiles = new List<Tile>();
        foreach (int tile in selectedTile)
            tiles.Add(BoardTiles[tile]);
        return tiles;
    }
}

public enum Move
{
    BenchToBench,
    BenchToBoard,
    BoardToBench,
    BoardToBoard,
    None
}
public abstract class BaseState
{
    protected NewHero hero;
    protected GameObject gameObject;
    protected Transform transform;

    public BaseState(NewHero hero)
    {
        this.hero = hero;
        this.gameObject = hero.gameObject;
        this.transform = gameObject.transform;
    }

    public virtual void CycleStart() { }
    public abstract Type Tick(float cycleProgress);
    public virtual void CycleEnd() { }
}
public class MoveState : BaseState
{
    int xDif, yDif;
    int xDifAbs, yDifAbs;
    int distance, distanceSquared;
    int currentTile, targetTile, targetMoveTile;

    public MoveState(NewHero hero) : base(hero)
    { }

    public override void CycleStart()
    {
        targetMoveTile = currentTile;
        currentTile = hero.CurrentTile;
        CalculateMove();
    }

    public override Type Tick(float cycleProgress)
    {
        //lerp to target position
        transform.position = Vector3.Lerp(transform.position, hero.BoardTiles[targetMoveTile].spawnPosition, cycleProgress);
        if (currentTile == targetTile)
            return typeof(AttackState);
        return null;
    }

    public override void CycleEnd()
    {
        //update currentTile
        currentTile = targetMoveTile;
        hero.SetCurrentTile(currentTile);
        //reset tile state
        //boardManager.BoardTiles[targetMoveTile].TileState = TileState.Targetted;
        //reset targetMoveTile
        //hero.TargetMoveTile = -1;
    }
    
    protected void CalculateMove()
    {
        if(currentTile == targetTile)
        {
            hero.BoardTiles[currentTile].TileState = TileState.Occupied;
            return;
        }
        if (!FindTargetTile())
        {
            targetTile = currentTile;
            return;
        }

        xDifAbs = Mathf.Abs(xDif);
        yDifAbs = Mathf.Abs(yDif);
        int differrence = Mathf.Abs(xDifAbs - yDifAbs);
        //Debug.Log("xDif: " + xDif + "\t yDif: " + yDif + "\t targetTile: " + targetTile);
        if(differrence > 0)
        {
            targetMoveTile = currentTile + Mathf.Clamp(xDif, -2, 2) + (Mathf.Clamp(yDif, -2, 2) * 8);
        }
        else if (xDifAbs > yDifAbs)
        {
            targetMoveTile = currentTile + Mathf.Clamp(xDif, -3, 3);
        }
        else if (xDifAbs < yDifAbs)
        {
            targetMoveTile = currentTile + (Mathf.Clamp(yDif, -3, 3) * 8);
        }

        hero.BoardTiles[targetMoveTile].TileState = TileState.Targetted;
        hero.BoardTiles[currentTile].TileState = TileState.Available;
        hero.TargetMoveTile = targetMoveTile;
        Debug.Log("targetMoveTile: " + targetMoveTile);
    }

    private bool FindTargetTile()
    {
        //currentTile = heroController.CurrentTile;
        List<int> remainingTargets = new List<int>();
        foreach(NewHero targetHero in hero.TargetHeroes)
        {
            remainingTargets.Add(targetHero.TargetMoveTile);
        }

        while (remainingTargets.Count > 0)
        {
            int targetHeroTile = GetClosestHeroTile(remainingTargets);
            if (GetClosestAvailableTile(targetHeroTile, out int availableTile))
            {
                SetNewTarget(availableTile);
                return true;
            }
            else
                remainingTargets.Remove(targetHeroTile);
        }
        Debug.Log("No available tile");
        return false;
    }
    
    private int GetClosestHeroTile(List<int> targetTiles)
    {
        int targetTile = -1;
        xDif = yDif = 0;
        float squaredDistance, closestDistance = float.MaxValue;

        foreach (int tile in targetTiles)
        {
            squaredDistance = CalculateSquaredTileDistance(tile, out int xDifference, out int yDifference);
            if (squaredDistance < closestDistance)
            {
                closestDistance = squaredDistance;
                targetTile = tile;
                xDif = xDifference;
                yDif = yDifference;
            }
        }
        return targetTile;
    }

    private bool GetClosestAvailableTile(int targetHeroTile, out int closestAvailableTile)
    {
        closestAvailableTile = -1;
        float closestDistance = float.MaxValue;
        float squaredDistance;

        //gets all tiles surrounding the targetHeroTile
        GetSurroundingTiles(targetHeroTile, out List<int> surroundingTiles);

        foreach (int tile in surroundingTiles)
        {
            squaredDistance = CalculateSquaredTileDistance(tile, out int xDifference, out int yDifference);
            if (squaredDistance >= closestDistance)
                continue;

            if (TileAvailable(tile, out int availableTile))
            {
                closestDistance = squaredDistance;
                closestAvailableTile = availableTile;
            }
        }

        if (closestAvailableTile >= 0)
            return true;
        return false;
    }

    public void GetSurroundingTiles(int targetHeroTile, out List<int> surroundingTiles)
    {
        surroundingTiles = new List<int>();
        surroundingTiles.Add(targetHeroTile - 1); //left tile
        surroundingTiles.Add(targetHeroTile + 7); //top left tile
        surroundingTiles.Add(targetHeroTile + 8); //top tile
        surroundingTiles.Add(targetHeroTile + 9); //top right tile
        surroundingTiles.Add(targetHeroTile + 1); //right tile
        surroundingTiles.Add(targetHeroTile - 7); //bottom right tile
        surroundingTiles.Add(targetHeroTile - 8); //bottom tile
        surroundingTiles.Add(targetHeroTile - 9); //bottom left tile

        if (targetHeroTile % 8 == 0)
        {
            //left side
            surroundingTiles.Remove(targetHeroTile +7); //top left tile
            surroundingTiles.Remove(targetHeroTile -1); //left tile
            surroundingTiles.Remove(targetHeroTile -9); //bottom left tile
        }
        if(targetHeroTile % 8 == 7)
        {
            //right side
            surroundingTiles.Remove(targetHeroTile + 9); //top right tile
            surroundingTiles.Remove(targetHeroTile + 1); //right tile
            surroundingTiles.Remove(targetHeroTile - 7); //bottom right tile
        }
        if(targetHeroTile <= 7)
        {
            //bottom
            surroundingTiles.Remove(targetHeroTile - 9); //bottom left tile
            surroundingTiles.Remove(targetHeroTile - 8); //bottom tile
            surroundingTiles.Remove(targetHeroTile - 7); //bottom right tile
        }
        if(targetHeroTile >= 56)
        {
            //top
            surroundingTiles.Remove(targetHeroTile + 7); //top left tile
            surroundingTiles.Remove(targetHeroTile + 8); //top tile
            surroundingTiles.Remove(targetHeroTile + 9); //top right tile
        }
        if (hero.Team == Team.Player)
            Debug.Log("player");
        else
            Debug.Log("enemy");
        Debug.Log("targetTile: " + targetHeroTile);
        foreach (int tile in surroundingTiles)
            Debug.Log("surrTile: " + tile);
    }

    public bool TileAvailable(int targetTile, out int availableTile)
    {
        availableTile = -1;
        if (hero.BoardTiles[targetTile].TileState == TileState.Available)
        {
            availableTile = targetTile;
            return true;
        }
        return false;
    }

    private void SetNewTarget(int targetTile)
    {
        xDif = (targetTile % 8) - (currentTile % 8);
        yDif = Mathf.RoundToInt(targetTile / 8) - Mathf.RoundToInt(currentTile / 8);
        this.targetTile = targetTile;
    }

    private float CalculateSquaredTileDistance(int targetTile, out int xDifference, out int yDifference)
    {
        xDifference = (targetTile % 8) - (currentTile % 8);
        yDifference = Mathf.RoundToInt(targetTile / 8) - Mathf.RoundToInt(currentTile / 8);
        return (xDifference * xDifference) + (yDifference * yDifference);
    }
}

Battlegrounds

Hearthstone Battlegrounds prototype

[RequireComponent(typeof(CardUI))]
public class Card : MonoBehaviour
{
    public CardCollection CurrentCardCollection { get; private set; }
    public CardData CardData { get; private set; }
    
    private CardUI _cardUI;

    private void Awake()
    {
        _cardUI = GetComponent<CardUI>();
    }

    public void SetCurrentCardCollection(CardCollection cardCollection)
    {
        CurrentCardCollection = cardCollection;
        transform.SetParent(cardCollection.transform);
    }

    public void SetDataAndUI(CardData data)
    {
        CardData = data;
        _cardUI.AssignUI(data);
    }
    
    public void MoveTo(Vector3 targetPosition, float duration)
    {
        //DOTween.Clear();
        transform.DOLocalMove(targetPosition, duration);
    }
}
[RequireComponent(typeof(NewCardLayoutGroup))]
public abstract class CardCollection : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
    public event EventHandler OnMouseEnter;
    public event EventHandler OnMouseExit;

    [SerializeField]
    protected GameObject _cardPrefab = null;

    [SerializeField]
    protected int _maxNumberOfCards = 8;

    protected List<Card> _cards = new List<Card>();
    private Vector3[] _storedPositions;

    private Card _potentialCard;
    private Vector3 _availablePosition;
    
    private ChangeTrackingWrapper<int> _availableCardIndex;
    protected ChangeTrackingWrapper<int> _numberOfCards;

    private NewCardLayoutGroup _newCardLayoutGroup;
    private Canvas _canvas;

    private float moveDuration = 1f;
    
    protected virtual void Awake()
    {
        _newCardLayoutGroup = GetComponent<NewCardLayoutGroup>();
        while (transform.parent != null)
        {
            _canvas = GetComponentInParent<Canvas>();
            if (_canvas != null)
                break;
        }

        _availableCardIndex = new ChangeTrackingWrapper<int>();
        _numberOfCards = new ChangeTrackingWrapper<int>();
    }

    protected virtual void Start()
    {
        _storedPositions = _newCardLayoutGroup.CalculateCardPositions(_numberOfCards.Value);
    }

    public virtual void AddCard(Card card)
    {
        if(_numberOfCards.Value == _maxNumberOfCards)
        {
            Debug.LogWarning("Can't add card to collection, because the collection already has the maximum number of cards");
            return;
        }
        card.SetCurrentCardCollection(this);
        //card.MoveTo(Vector3.zero, moveDuration);
        //_cards.Add(card);
        _numberOfCards.Value++;
        //insert card at available index
        _storedPositions = _newCardLayoutGroup.CalculateCardPositions(_numberOfCards.Value);
        _availableCardIndex.Value = _newCardLayoutGroup.CardIndexClosestToMouse(_storedPositions);
        _cards.Insert(_availableCardIndex.Value, card);
        ResetLayout();
        //move card to available position
    }

    public void SimpleUpdateLayoutMovingCard(Card card)
    {
        _storedPositions = _newCardLayoutGroup.CalculateCardPositions(_numberOfCards.Value);
        _availableCardIndex.Value = _newCardLayoutGroup.CardIndexClosestToMouse(_storedPositions);
        _availablePosition = _storedPositions[_availableCardIndex.Value];
        MoveUsingDynamicLayout(_storedPositions, _availableCardIndex.Value);
        //card.MoveTo(_availablePosition, moveDuration);
    }
    
    public virtual void RemoveCard(Card card)
    {
        _cards.Remove(card);
        _numberOfCards.Value--;
        ResetLayout();
    }

    public void DraggingCardFromThisCollection(Card card)
    {
        int cardIndex = _cards.IndexOf(card);

    }

    public void DraggingCardFromOtherCollection(Card card)
    {
        //stored positions needs to be called only once
        _storedPositions = _newCardLayoutGroup.CalculateCardPositions(_numberOfCards.Value + 1);
        //availableCardIndex needs to be updated constantly to check if cards need to be moved around
        _availableCardIndex.Value = _newCardLayoutGroup.CardIndexClosestToMouse(_storedPositions);
        //cards only need to be moved around when the availableCardIndex has changed
        ResetLayout();
    }

    public void UpdateLayoutWithMovingCard(Card card)
    {
        if(_numberOfCards.HasChanged)
        {
            _storedPositions = _newCardLayoutGroup.CalculateCardPositions(_numberOfCards.Value);
            _numberOfCards.ResetChangedFlag();
        }
        
        _availableCardIndex.Value = _newCardLayoutGroup.CardIndexClosestToMouse(_storedPositions);
        if(_availableCardIndex.HasChanged)
        {
            _availablePosition = _storedPositions[_availableCardIndex.Value];
            _availableCardIndex.ResetChangedFlag();
        }
        
        if (!_availableCardIndex.HasChanged) { return; }

        //move items to new position
        MoveUsingDynamicLayout(_storedPositions, _availableCardIndex.Value);
        _availableCardIndex.ResetChangedFlag();
    }

    public void ResetLayout()
    {
        Vector3[] positions = _newCardLayoutGroup.CalculateCardPositions(_numberOfCards.Value);
        for (int i = 0; i < _numberOfCards.Value; i++)
        {
            _cards[i].MoveTo(positions[i], moveDuration);
        }
    }

    public void ResetLayoutWithoutDraggedCard(Card card)
    {
        Vector3[] positions = _newCardLayoutGroup.CalculateCardPositions(_numberOfCards.Value);
        for (int i = 0; i < _cards.Count; i++)
        {
            if (_cards[i].Equals(card))
                continue;
            _cards[i].MoveTo(positions[i], moveDuration);
        }
    }

    private void MoveUsingDynamicLayout(Vector3[] positions, int closestElement)
    {
        for (int i = 0; i < _numberOfCards.Value; i++)
        {
            if (i < closestElement)
            {
                _cards[i].MoveTo(positions[i], moveDuration);
            }
            else
            {
                _cards[i].MoveTo(positions[i + 1], moveDuration);
            }
            _cards[i].gameObject.SetActive(true);
        }
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        OnMouseEnter(this, EventArgs.Empty);
        //mouse inside area
        if(Input.GetMouseButton(0))
        {
            //check for potential card being dragged
        }
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        OnMouseExit(this, EventArgs.Empty);
    }
}
public class NewCardLayoutGroup : MonoBehaviour
{
    [SerializeField]
    protected float _horizontalSpacing = 150, _verticalSpacing = 0;

    private float _halfWidth = 0, _halfHeight = 0, _scaleFactor = 0;

    private Vector3[] _cardPositions;
    private ChangeTrackingWrapper<int> _numberOfCards;

    private Camera _cam;
    private Canvas _canvas;

    private void Awake()
    {
        _cam = Camera.main;
        while (transform.parent != null)
        {
            _canvas = GetComponentInParent<Canvas>();
            if (_canvas != null)
                break;
        }
    }

    private void Start()
    {
        _halfWidth = Screen.width / 2f;
        _halfHeight = Screen.height / 2f;
        _scaleFactor = _canvas.scaleFactor;

        _numberOfCards = new ChangeTrackingWrapper<int>();
    }

    public Vector3[] CalculateCardPositions(int numberOfCards)
    {
        Vector3[] positions = new Vector3[numberOfCards];
        
        float centerIndex = GetArrayCenterIndex(numberOfCards);

        for (int i = 0; i < numberOfCards; i++)
        {
            float xPosition = (centerIndex - i) * _horizontalSpacing;
            float yPosition = (centerIndex - i) * _verticalSpacing;
            positions[i] = new Vector3(xPosition, yPosition, 0f);
        }

        return positions;
    }

    public Vector3[] GetPositionOnChange(int numberOfCards)
    {
        _numberOfCards.Value = numberOfCards;
        if (!_numberOfCards.HasChanged) { return _cardPositions; }

        _numberOfCards.ResetChangedFlag();
        _cardPositions = CalculateCardPositions(numberOfCards);
        return _cardPositions;
    }

    protected float GetArrayCenterIndex(int numberOfCards)
    {
        float centerIndex;
        bool numberOfCardsIsEven = numberOfCards % 2 == 0 ? true : false;

        if (numberOfCardsIsEven)
        {
            int lastElement = numberOfCards - 1;
            centerIndex = lastElement / 2f;
        }
        else
        {
            centerIndex = Mathf.FloorToInt(numberOfCards / 2f);
        }
        return centerIndex;
    }

    public int CardIndexClosestToMouse(Vector3[] cardPositions)
    {
        Vector3 rawMousePos = Input.mousePosition;
        Vector3 mousePos = new Vector3((rawMousePos.x - _halfWidth) / _scaleFactor, 0f, 0f);

        //TODO: start at -1, with exception when the cardPositions array is empty, then return 0
        int closestElement = 0;
        float shortestDistance = Mathf.Infinity;

        for (int i = 0; i < cardPositions.Length; i++)
        {
            float distance = Mathf.Abs(mousePos.x - cardPositions[i].x);
            if (distance < shortestDistance)
            {
                shortestDistance = distance;
                closestElement = i;
            }
        }

        return closestElement;
    }
}
public class CardCollectionManager : MonoBehaviour
{
    private static CardCollection _draggedCardOriginalCollection;
    private static CardCollection _pointerOverCollection;

    private static Card _draggedCard;

    private static bool _isDragging;

    private CardCollection[] _cardCollections;
    
    private void Awake()
    {
        _cardCollections = GetComponentsInChildren<CardCollection>();
        foreach(CardCollection cardCollection in _cardCollections)
        {
            cardCollection.OnMouseEnter += PointerEnterCollection;
            cardCollection.OnMouseExit += PointerExitCollection;
        }
    }
    
    private void FixedUpdate()
    {
        if(_isDragging)
        {
            if(_pointerOverCollection == _draggedCardOriginalCollection)
            {
                Debug.Log("default collection");
                _pointerOverCollection.SimpleUpdateLayoutMovingCard(_draggedCard);
            }
            else if(_pointerOverCollection == null)
            {
                Debug.Log("pointer over nothing, default collection selected");
                _draggedCardOriginalCollection.SimpleUpdateLayoutMovingCard(_draggedCard);
            }
            else
            {
                Debug.Log("pointer over different collection");
            }
        }
    }

    public void PointerEnterCollection(object obj, EventArgs e)
    {
        CardCollection cardCollection = obj as CardCollection;
        _pointerOverCollection = cardCollection;

        if(_isDragging)
        {
            //move cards from original collection back
            if(!_pointerOverCollection.Equals(_draggedCardOriginalCollection))
            {
                _draggedCardOriginalCollection.ResetLayoutWithoutDraggedCard(_draggedCard);
            }
        }
    }

    public void PointerExitCollection(object obj, EventArgs e)
    {
        if(_isDragging && !_pointerOverCollection.Equals(_draggedCardOriginalCollection))
        {
            //set cards back to default position
            _pointerOverCollection.ResetLayout();
        }

        _pointerOverCollection = null;
    }

    public static void StartDrag(Card card)
    {
        _draggedCardOriginalCollection = card.CurrentCardCollection;
        _isDragging = true;
        _draggedCard = card;
    }

    public static void StopDrag(Card card)
    {
        if(_pointerOverCollection == null)
        {
            //move card back to original collection and position
            //_draggedCard.MoveTo(_draggedCardOriginalCollection.GetAvailablePosition());
            _draggedCardOriginalCollection.ResetLayout();
        }
        else
        {
            //transfer card to new card collecion
            _draggedCardOriginalCollection.RemoveCard(card);
            _pointerOverCollection.AddCard(card);
            //move card to available position
        }
        _draggedCardOriginalCollection = null;

        _isDragging = false;
        _draggedCard = null;
    }
}
[RequireComponent(typeof(Card), typeof(CanvasGroup))]
public class CardInteractions : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IDragHandler, IBeginDragHandler, IEndDragHandler
{
    private Card _card;
    private CanvasGroup _canvasGroup;

    private void Awake()
    {
        _card = GetComponent<Card>();
        _canvasGroup = GetComponent<CanvasGroup>();
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        //glow, grow, move to front, move others away
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        //stop glow, shrink, move back in line
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        _canvasGroup.blocksRaycasts = false;
    }

    public void OnDrag(PointerEventData eventData)
    {
        //follow mouse
        transform.position = eventData.position;
        CardCollectionManager.StartDrag(_card);
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        _canvasGroup.blocksRaycasts = true;
        CardCollectionManager.StopDrag(_card);

    }
}

https://github.com/CasLuitwieler/HS-Battlegrounds

Mirror project

Mirror project with combat and interactable objects

https://github.com/CasLuitwieler/Mirror-Networking

[RequireComponent(typeof(NetworkIdentity))]
public class PlayerID : NetworkBehaviour
{
    private string _playerID = "not yet assigned";

    public override void OnStartLocalPlayer()
    {
        CreatePlayerID();
        transform.name = _playerID;
        CmdSetPlayerID(_playerID);
    }

    private void CreatePlayerID()
    {
        _playerID = "Player " + GetComponent<NetworkIdentity>().netId.ToString();
    }

    [Command]
    private void CmdSetPlayerID(string playerID)
    {
        transform.name = playerID;
        NetworkIDManager.AddPlayer(playerID, this.gameObject);
    }
}
[RequireComponent(typeof(InputReader))]
public class PlayerCombatBehaviour : NetworkBehaviour
{
    [SerializeField]
    private float _hitRange = 2f, _damageAmount = 10f;

    [SerializeField]
    private LayerMask _damageableObjectsLayer = -1;

    private Ray _ray;
    private Camera _cam;
    private CameraController _camController;
    private InputReader _inputReader;

    private void Awake()
    {
        _inputReader = GetComponent<InputReader>();
    }

    private void Start()
    {
        if (!isLocalPlayer)
            return;

        _cam = FindObjectOfType<Camera>();
        _camController = FindObjectOfType<CameraController>();
    }

    private void Update()
    {
        if (!isLocalPlayer)
            return;

        _ray = _cam.ScreenPointToRay(Input.mousePosition);
        Debug.DrawRay(_ray.origin, _ray.direction * (_hitRange + _camController.DistanceToTarget));

        HandleInput();
    }

    private void HandleInput()
    {
        if (_inputReader.DamageKeyDown())
        {
            _ray = _cam.ScreenPointToRay(Input.mousePosition);
            if (!Physics.Raycast(_ray, out RaycastHit hit, _hitRange + _camController.DistanceToTarget, _damageableObjectsLayer))
                return;

            Debug.Log("hit object " + hit.transform.gameObject.name);

            if (hit.transform.TryGetComponent(out NetworkIdentity networkID))
            {
                //damage the object that was hit
                CmdDealDamage(networkID);
            }
            else
            {
                if (hit.transform.TryGetComponent(out IDamageable damageable))
                {
                    Debug.LogError("Dealing damage to an object that doesn't have a network identity");
                    damageable.TakeDamge(_damageAmount);
                }
            }
        }
    }

    [Command]
    private void CmdDealDamage(NetworkIdentity networkID)
    {
        GameObject target = NetworkIDManager.GetPlayer(networkID.netId.ToString());

        if (target.TryGetComponent(out IDamageable damageable))
        {
            Debug.Log("Found IDamageable");
            damageable.TakeDamge(_damageAmount);
        }
    }
}
public class PlayerHealthBehaviour : NetworkBehaviour, IDamageable
{
    public delegate void OnPlayerDied();
    public delegate void OnHealthChanged(float healthPercentage);

    //TODO: when new player joins, show the correct health value
    [SyncEvent]
    public event OnHealthChanged EventHealthChanged;

    [SyncEvent]
    public event OnPlayerDied EventPlayerDied;

    [SyncVar]
    private float _health;

    [SerializeField]
    private float _maxHealth = 100f;

    private bool isDead = false;
    private HealthBar _healthBar;

    private void Awake()
    {
        _health = _maxHealth;
    }

    public void TakeDamge(float damageAmount)
    {
        if (isDead)
            return;

        _health -= damageAmount;
        float healthPercentage = _health / _maxHealth;
        EventHealthChanged?.Invoke(healthPercentage);

        if (_health <= 0)
        {
            isDead = true;
            EventPlayerDied?.Invoke();
            DestroyAuthorisedObjects(GetComponent<NetworkIdentity>());
        }
    }

    private void DestroyAuthorisedObjects(NetworkIdentity networkIdentity)
    {
        //NetworkServer.Destroy(this.gameObject);
        NetworkServer.DestroyPlayerForConnection(networkIdentity.connectionToClient);
    }
}
[RequireComponent(typeof(SphereCollider))]
public class InteractableObject : MonoBehaviour, IInteractable
{
    [SerializeField]
    private float _indicatorRange = 8f, _indicatorFadeDuration = 0.25f;

    [SerializeField]
    private GameObject _indicator = null;
    
    private SphereCollider _indicatorTrigger;
    private Image _indicatorImage;

    private void Awake()
    {
        _indicatorTrigger = GetComponent<SphereCollider>();
        _indicatorTrigger.isTrigger = true;
        _indicatorTrigger.radius = _indicatorRange;

        _indicatorImage = _indicator.GetComponentInChildren<Image>();

        Color color = _indicatorImage.color;
        color.a = 0f;
        _indicatorImage.color = color;
    }

    public void OnStartHover()
    {
        Debug.Log("Start hover");
    }

    public void OnInteract()
    {
        Debug.Log("OnInteract triggered");
    }
    
    public void OnEndHover()
    {
        Debug.Log("End hover");
        
        //if player within range indicator range, enable indicator UI
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.TryGetComponent(out DetectInteractableObjects player))
        {
            //show indicator UI sprite of interactable object for this player
            _indicatorImage.DOFade(1f, _indicatorFadeDuration);
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.TryGetComponent(out DetectInteractableObjects player))
        {
            //hide indicator UI sprite of interactable object for this player
            _indicatorImage.DOFade(0f, _indicatorFadeDuration);
        }
    }
}
public class DetectInteractableObjects : NetworkBehaviour
{
    [SerializeField]
    private float _interactRange = 3f;

    private IInteractable _currentInteractableTarget;
    private Transform _currentTargetTransform;

    private Ray _ray;
    private Camera _cam;    

    private void Start()
    {
        _cam = FindObjectOfType<Camera>();
    }

    private void Update()
    {
        if (!isLocalPlayer)
            return;

        RaycastForward();
    }

    private void RaycastForward()
    {
        _ray = _cam.ScreenPointToRay(Input.mousePosition);
        if (!Physics.Raycast(_ray, out RaycastHit hit, _interactRange))
            EndInteraction();
        else if(_currentTargetTransform == null)                            //start looking at a new object
        {
            StartInteraction(hit.transform);
        }
        else if(_currentTargetTransform != hit.transform)                   //switched looking between objects
        {
            EndInteraction();
            StartInteraction(hit.transform);
        }
    }
    
    private void StartInteraction(Transform newObjectTransform)             //when looking at a new object or entering it's detection range
    {
        _currentTargetTransform = newObjectTransform;
        if (TryGetComponent(out IInteractable interactable))
        {
            _currentInteractableTarget = interactable;
            _currentInteractableTarget.OnStartHover();
        }
    }

    private void EndInteraction()                                           //when looking away from an object or going out range
    {
        if (_currentInteractableTarget == null)
            return;

        _currentInteractableTarget.OnEndHover();
        _currentInteractableTarget = null;
        _currentTargetTransform = null;
    }
}
public class HealthSystem : IDamageable, IHealable
{
    private readonly int _maxHealth;

    public HealthSystem(int maxHealth)
    {
        _maxHealth = maxHealth;
        Health = maxHealth;
    }

    public event EventHandler<HealthChangedArgs> HealthChanged = delegate { };
    public event EventHandler Died = delegate { };

    public bool IsDead { get; private set; } = false;
    public float Health { get; private set; } = 0;
    public float MaxHealth => _maxHealth;

    public void TakeDamge(float damageAmount)
    {
        if (IsDead) { return; }
        if (damageAmount <= 0) { return; }

        Health = Mathf.Max(0, Health - (int)damageAmount);
        HealthChanged(this, new HealthChangedArgs(Health, MaxHealth));

        if (Health == 0)
        {
            IsDead = true;
            Died(this, EventArgs.Empty);
        }
    }

    public void Heal(int healAmount)
    {
        if (IsDead) { return; }
        if (healAmount <= 0) { return; }

        Health = Mathf.Min(_maxHealth, Health + healAmount);
        HealthChanged(this, new HealthChangedArgs(Health, _maxHealth));
    }
}
public class HealthBar : MonoBehaviour
{
    [SerializeField]
    private Image foregroundImage = null;

    [SerializeField]
    private float changeHealthTweenDuration = 1f;

    private NetworkIdentity _networkIdentity;

    private void Awake()
    {
        _networkIdentity = GetComponentInParent<NetworkIdentity>();
    }

    private void Start()
    {
        GetComponentInParent<PlayerHealthBehaviour>().EventHealthChanged += (x) => HandleHealthChanged(x);
        if (_networkIdentity.isLocalPlayer)
            gameObject.SetActive(false);
    }

    private void HandleHealthChanged(float targetPercentage)
    {
        foregroundImage.DOFillAmount(targetPercentage, changeHealthTweenDuration);
    }
}

MOBA

MOBA prototype I made in the summer of 2018

public class BaseSpell : MonoBehaviour
{
    [SerializeField]
    private SpellType[] allSpellTypes;

    [HideInInspector]
    public Dictionary<int, Spell> allSpells = new Dictionary<int, Spell>();

    void Start()
    {
        for(int i = 0; i < allSpellTypes.Length; i++)
        {
            SpellType newSpellType = allSpellTypes[i];
            Spell newSpell = new Spell(i, newSpellType.spellName, newSpellType.spellManaCost, newSpellType.spellCooldown, newSpellType.fromOrigin);
            allSpells[i] = newSpell;
        }
    }

}

public class Spell
{
    public int spellID;
    public string spellName;
    public int spellManaCost;
    public float spellCooldown;
    public bool fromOrigin;

    public Spell(int id, string name, int manaCost, float cooldown, bool origin)
    {
        spellID = id;
        spellName = name;
        spellManaCost = manaCost;
        spellCooldown = cooldown;
        fromOrigin = origin;
    }
}

[System.Serializable]
public class SpellType
{
    public string spellName;
    public int spellManaCost;
    public float spellCooldown;
    public bool fromOrigin;
}
public class ProjectileScript : MonoBehaviour
{
    public ParticleSystem projectile;

    public GameObject parent;

    public GameObject enemy;
    public GameObject monster;

    public GameObject impactEffect;

    private List<ParticleCollisionEvent> collisionEvents;

    private float elapsed = 0f;
    private float spawnTimer = 0.8f;

    private int index = 0;

    void Start()
    {
        GetComponent<ParticleSystemRenderer>().enabled = false;
        for(int i = 0; i < GetComponentsInChildren<ParticleSystemRenderer>().Length; i++)
        {
            GetComponentsInChildren<ParticleSystemRenderer>()[i].enabled = false;
        }
        collisionEvents = new List<ParticleCollisionEvent>();
        parent.transform.position += this.transform.right;
    }

    private void Update()
    {
        elapsed += Time.deltaTime;
        Debug.Log("test");
        if(elapsed >= spawnTimer)
        {
            GetComponent<ParticleSystemRenderer>().enabled = true;
            for (int i = 0; i < GetComponentsInChildren<ParticleSystemRenderer>().Length; i++)
            {
                GetComponentsInChildren<ParticleSystemRenderer>()[i].enabled = true;
            }
            parent.transform.position += this.transform.right * 5f * Time.deltaTime;
        }

    }

    private void OnParticleCollision(GameObject other)
    {
        Debug.Log("Hit " + other.gameObject.name.ToString());
        if(other.gameObject.tag == "Enemy" || other.gameObject.tag == "JungleMonster")
        {
            //store collision data
            ParticlePhysicsExtensions.GetCollisionEvents(projectile, other, collisionEvents);

            SetImpactEffect(collisionEvents[index]);

            index++;

            //trigger hurt and death animations on enemy
            //other.GetComponent<EnemyController>().Colliding();

            other.GetComponent<CombatManager>().TakeDamage(50, "magic");

            Destroy(parent);
        }
    }

    void SetImpactEffect(ParticleCollisionEvent particleCollisionEvent)
    {
        GameObject ImpactEffect = (GameObject)Instantiate(impactEffect, particleCollisionEvent.intersection, Quaternion.identity);
        Destroy(ImpactEffect, ImpactEffect.GetComponent<ParticleSystem>().main.duration + ImpactEffect.GetComponent<ParticleSystem>().main.startLifetime.constantMax);
    }

}
public class CombatManager : MonoBehaviour
{
    public float health;
    private float maxHealth = 100f;
    private float healthRegen = 1f;
    private float healthRegenTimer = 1f;
    private float healthTimerElapsed;

    private int armor = 0;
    private int magicRes = 0;

    private int attackDmg;
    private int attackSpd;

    private int magicDmg;
    private int magicPen;

    private bool death;
    private float bodyDisappearTimer = 10f;
    private float bodyTimerElapsed;

    private float fadeOutTimer;
    private float amountOfFadeSteps = 100f;
    private float fadeTimerElapsed;
    private MeshRenderer meshRenderer;

    void Awake()
    {
        if(GetComponent<MeshRenderer>())
            meshRenderer = this.GetComponent<MeshRenderer>();
    }

    void Start()
    {
        health = maxHealth;
        fadeOutTimer = (bodyDisappearTimer / 2) / amountOfFadeSteps;
    }

    void Update()
    {
        HealthCheck();
    }

    void HealthCheck()
    {
        if (health <= 0)
        {
            Death();
        }
        else if (health < maxHealth)
        {
            healthTimerElapsed += Time.deltaTime;
            if(healthTimerElapsed >= healthRegenTimer)
            {
                health += healthRegen;
                healthTimerElapsed = 0f;
            }
        }
        else
        {
            health = maxHealth;
        }

    }

    public void TakeDamage(float dmg, string dmgType)
    {
        if (dmgType == "magic")
        {
            dmg -= magicRes;
        }
        else if (dmgType == "physical")
        {
            dmg -= armor;
        }
        else
            Debug.LogError("Invalid dmg type");

        if(health - dmg > 0)
        {
            health -= dmg;
        }
        else
        {
            health = 0;
        }
    }

    public float GetHealth()
    {
        return health;
    }

    public void Heal(float amount)
    {
        if(health + amount < maxHealth)
        {
            health += amount;
        }
        else
        {
            health = maxHealth;
        }
    }

    void Death()
    {
        Debug.Log("Play death animation");
        bodyTimerElapsed += Time.deltaTime;

        if (bodyTimerElapsed >= (bodyDisappearTimer / 2f))
        {
            FadeOut();

        }
        if (bodyTimerElapsed >= bodyDisappearTimer)
        {
            if(this.gameObject.tag == "JungleMonster")
            {
                this.gameObject.SetActive(false);
            }
            else
                Destroy(this.gameObject);
        }
    }

    void FadeOut()
    {
        fadeTimerElapsed += Time.deltaTime;

        //every (1 / amountOfFadeSteps) seconds
        if (fadeTimerElapsed >= fadeOutTimer)
        {
            Color c = meshRenderer.material.color;

            //fade for (1 / amountOfFadeSteps) of the alpha
            if (c.a > 0)
            {
                
                c.a -= (1 / amountOfFadeSteps);
            }
            else
                c.a = 0;

            meshRenderer.material.color = c;

            fadeTimerElapsed = 0f;
        }
    }

}

FPS visual effects

FPS demo with bullet impacts and different fire modes.

public class ImpactEffect : MonoBehaviour, IShootable
{
    [SerializeField] private GameObject _hitEffect = null;

    public void PlayHitEffect(Vector3 position, Vector3 normal)
    {
        GameObject impactEffect = (GameObject)Instantiate(_hitEffect, position, Quaternion.LookRotation(normal));
        //impactEffect.GetComponent<ParticleSystem>().Play();
        Destroy(impactEffect, 5f);
    }
}
public class BurstFire : MonoBehaviour, IFireMode
{
    public event Action OnFire;

    [SerializeField] private float _fireRate = 0.5f, _burstFireRate = 0.1f;
    [SerializeField] private int _nBurstBullets = 3;

    private int _currentBullet = 0;
    private float _nextTimeToFire = 0;

    private Coroutine _burstCoroutine;
    private bool _isBursting = false;

    private IWeaponInput _weaponInput;

    private void Awake()
    {
        _weaponInput = GetComponent<IWeaponInput>();
    }

    public bool CanFire()
    {
        if (_weaponInput == null) { Debug.LogWarning("FireMode couldn't find WeaponInput component"); return false; }

        if (!_weaponInput.FireKeyDown()) { return false; }
        if (Time.time < _nextTimeToFire) { return false; }
        return true;
    }

    public void Fire()
    {
        if(_isBursting) { Debug.Log("Enter Burst Fire() while still in burst"); }
        _nextTimeToFire += 999f;
        _burstCoroutine = StartCoroutine(Burst());
    }

    private IEnumerator Burst()
    {
        _isBursting = true;
        while(_currentBullet < _nBurstBullets)
        {
            OnFire?.Invoke();
            _currentBullet++;
            yield return new WaitForSeconds(_burstFireRate);
        }

        _currentBullet = 0;
        _nextTimeToFire = Time.time + _fireRate;
        _isBursting = false;
    }
}
public class WeaponController : MonoBehaviour
{
    [SerializeField] private float _bulletDistance = 100f, _damageAmount = 10f;
    [SerializeField] private ParticleSystem _muzzleFlash = null, _bulletTracer = null;

    private int _currentFireModeIndex, _nFireModes;
    private bool _hasMultipleFireModes = false;

    private IFireMode _currentFireMode;
    private IFireMode[] _fireModes;

    private Camera _cam;

    private void Awake()
    {
        _cam = Camera.main;
        SetupFireModes();
    }

    private void Update()
    {
        DrawDebugRay();

        if(_currentFireMode == null) { return; }

        if (Input.GetKeyDown(KeyCode.V) && _hasMultipleFireModes)
        {
            //cycle fire mode
            Debug.Log("Previous index: " + _currentFireModeIndex);

            _fireModes[_currentFireModeIndex].OnFire -= Shoot;

            _currentFireModeIndex++;
            if (_currentFireModeIndex >= _nFireModes)
                _currentFireModeIndex = 0;
            _currentFireMode = _fireModes[_currentFireModeIndex];

            _fireModes[_currentFireModeIndex].OnFire += Shoot;
            Debug.Log("New index: " + _currentFireModeIndex);
        }

        if (_currentFireMode.CanFire())
        {
            _currentFireMode.Fire();
        }
    }

    private void Shoot()
    {
        _muzzleFlash.Play();
        _bulletTracer.Play();
        Vector3 rayStartPos = _cam.ViewportToWorldPoint(new Vector3(0.5f, 0.5f, 0f));
        if(Physics.Raycast(rayStartPos, _cam.transform.forward, out RaycastHit hit, _bulletDistance))
        {
            if(hit.transform.TryGetComponent(out IShootable shootable))
            {
                shootable.PlayHitEffect(hit.point, hit.normal);
            }

            if(!hit.transform.TryGetComponent(out IDamageable damageable)) { return; }

            damageable.TakeDamage(_damageAmount);
        }
    }

    private void SetupFireModes()
    {
        _fireModes = GetComponentsInChildren<IFireMode>();

        _nFireModes = _fireModes.Length;
        if (_nFireModes == 0) { Debug.LogWarning("No firemode attached to weapon"); return; }
        else if(_nFireModes > 1) { _hasMultipleFireModes = true; }
        _currentFireModeIndex = 0;
        _currentFireMode = _fireModes[_currentFireModeIndex];
        _fireModes[_currentFireModeIndex].OnFire += Shoot;
    }

    private void DrawDebugRay()
    {
        Vector3 mousePos = _cam.ViewportToWorldPoint(new Vector3(0.5f, 0.5f, 0f));
        Debug.DrawRay(mousePos, _cam.transform.forward * _bulletDistance, Color.yellow);
    }
}

Asteroid game