//a method that sorts a list of units depending on the sort type and return a chained sorted list of the sorted units private ChainedSortedList <float, Unit> SortUnits(List <Unit> units, UnitSortTypes sortType, float offsetRadius, AttackModes attackMode = AttackModes.none, FactionEntity targetEntity = null, Vector3 targetPosition = default, bool playerCommand = false) { ChainedSortedList <float, Unit> sortedUnits = new ChainedSortedList <float, Unit>(); //this will hold the sorted list of the units. //attack only: //change the target? bool canChangeTarget = (attackMode == AttackModes.full || attackMode == AttackModes.change) ? true : false; //assigned attack only? bool assignedAttackOnly = (attackMode == AttackModes.full || attackMode == AttackModes.assigned) ? true : false; foreach (Unit unit in units) //go through the list of units. { if (unit.MovementComp.CanMove() == false) //if the unit can't move { if (sortType == UnitSortTypes.attackRange) //if this is an attack movement, just check if the target is in range or not { LaunchNonMovementAttackLocal(unit, targetEntity, playerCommand); continue; } else { if (playerCommand == true && unit.FactionID == GameManager.PlayerFactionID) //player notification message { ErrorMessageHandler.OnErrorMessage(ErrorMessage.notMovable, unit); } continue; } } float sortKey = unit.MovementComp.GetAgentRadius(); //sort key is set to agent radius by default if (sortType != UnitSortTypes.radius) //if we're not sorting by radius <=> we're not sorting for a simple movement but for an attack movement and therefore: { if (CanProceedAttackMovement(unit, playerCommand, assignedAttackOnly, canChangeTarget, targetEntity) == false) //do not proceed if the attack is not possible { continue; } sortKey = unit.AttackComp.RangeType.GetStoppingDistance(targetEntity == null ? EntityTypes.none : targetEntity.Type); //update sort key if (Vector3.Distance(unit.transform.position, targetPosition) < sortKey + offsetRadius) //if the attack unit is already inside its target range { //attack directly EnqueueMovementTask(new MovementTask { unit = unit, targetPosition = unit.transform.position, lookAtPosition = targetPosition, target = targetEntity, targetMode = InputMode.attack, attack = true, attackTarget = targetEntity, attackTargetPosition = targetPosition, }); //add a new movement task to the movement queue to attack directly continue; //move to next unit } } unit.MovementComp.TriggerTargetPositionCollider(false); //disable the target position collider first so that it won't intefer in the movement calculations sortedUnits.Add(sortKey, unit); //add the new alongside its radius as the key. } return(sortedUnits); }
//the PrepareMove method prepares the movement of a list of units by sorting them based on their radius, distance to target and generating target positions public void PrepareMove(List <Unit> units, Vector3 destination, float offsetRadius, Entity target, InputMode targetMode, bool playerCommand, bool attackMovement = false, AttackModes attackMode = AttackModes.none, FactionEntity targetEntity = null) { //create a new chained sorted list where units will be sorted based on their radius or stopping distance towards the target unit/building (in case of an attack) ChainedSortedList <float, Unit> radiusSortedUnits = SortUnits(units, attackMovement ? UnitSortTypes.attackRange : UnitSortTypes.radius, offsetRadius, attackMode, targetEntity, destination, playerCommand); float currOffsetRadius = offsetRadius; //set the current offset radius (we might need the same original offsetRadius value later) AudioClip movementOrderAudio = null; //we'll just pick one of the movement order audios from the units list if (playerCommand && (!targetEntity || attackMovement == false)) //if this is a player command and it's not an attack movement with an assigned target { SpawnTargetEffect(!attackMovement, destination); } foreach (KeyValuePair <float, List <Unit> > currUnits in radiusSortedUnits) //for each different list of units that share the same radius { if (attackMovement == true) //if this is an attack movement { currOffsetRadius += currUnits.Key; //increment the offset radius by the attack range (which is represented by the key in case) } //sort units in ascending order of the distance to the target position currUnits.Value.Sort((x, y) => Vector3.Distance(x.transform.position, destination).CompareTo(Vector3.Distance(y.transform.position, destination))); UnitMovement movementRef = currUnits.Value[0].MovementComp; //reference movement component Vector3 unitsDirection = Vector3.zero; //direction of units regarding the target position foreach (Unit unit in currUnits.Value) { unitsDirection += (destination - unit.transform.position).normalized; } unitsDirection /= currUnits.Value.Count; Formations currentFormation = GetFormation(currUnits.Value[0], attackMovement); //current movement formation for this units group List <Vector3> targetPositions = GenerateTargetPositions(currentFormation, destination, currUnits.Value.Count, movementRef.GetAgentRadius(), movementRef.GetAgentAreaMask(), movementRef.CanFly(), ref currOffsetRadius, unitsDirection); //get the target positions for the current units list for (int i = 0; i < currUnits.Value.Count; i++) //go through the sorted units list { if (targetPositions.Count == 0) //no more target positions available? { if (playerCommand == true && currUnits.Value[i].FactionID == GameManager.PlayerFactionID) //if this is a player command then inform the player { ErrorMessageHandler.OnErrorMessage(ErrorMessage.targetPositionNotFound, currUnits.Value[i]); } continue; } switch (currentFormation) { case Formations.circular: if (attackMovement == true) //only pick the closest target position in case this is an attack movement type and we're using the circular formation { targetPositions.Sort((x, y) => Vector3.Distance(x, currUnits.Value[i].transform.position).CompareTo(Vector3.Distance(y, currUnits.Value[i].transform.position))); //sort the target positions list depending on the distance to the actual unit to move } break; case Formations.rectangular: //if this is a rectangular formation, then the closest unit always gets to the farthest position targetPositions.Sort((x, y) => Vector3.Distance(y, currUnits.Value[i].transform.position).CompareTo(Vector3.Distance(x, currUnits.Value[i].transform.position))); //sort the target positions list depending on the distance to the actual unit to move break; } if (movementOrderAudio == null) //if the movement order audio hasn't been set yet { movementOrderAudio = attackMovement == true ? currUnits.Value[i].AttackComp.GetOrderAudio() : currUnits.Value[i].MovementComp.GetMvtOrderAudio(); //set it. } EnqueueMovementTask(new MovementTask { unit = currUnits.Value[i], targetPosition = targetPositions[0], lookAtPosition = targetPositions[0] + unitsDirection, target = target, targetMode = targetMode, attack = attackMovement, attackTarget = targetEntity, attackTargetPosition = destination, }); //add a new movement task to the movement queue targetPositions.RemoveAt(0); } } if (playerCommand) //if this is a player command { AudioManager.Play(gameMgr.GetGeneralAudioSource(), movementOrderAudio, false); //play the movement order audio } }