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