[Unity3D, C#] Rotacja wieży i Raycasty


#1

Dzisiaj zwracam się do was z prośbą o pomoc w następującym problemie.

Mam czołg, do którego podczepione są m. in. 2 elementy: wieża (Turret) oraz niewidzialny “celownik” (targetPointer). Pod względem hierarchii są takie same (oba podczepione do obiektu HoverTank). Mają taką samą rotację.

W kodzie są objaśnienia, ale co jest moim celem - tuż obok mojego czołgu znajduje się przeciwnik - ma podczepiony komponent ObjectStats ze zmienną bool isEnemy (w Inspectorze zaznaczyłem true i nie jest to resetowane). Żeby było prościej, znajduje się on w zasięgu strzału czołgu (range). Idea jest taka, by targetPointer obracał się o 1 stopień i sprawdzał, czy wróg jest na linii strzału. Jeśli nie, następuje kolejny obrót i kolejne sprawdzenie (max 360 cykli, wolę unikać pętli while). I tu jest pies pogrzebany - w ogóle ten obrót nie następuje. Rotacja wieży jest dodatkowo “zamrożona”, gdy do zmiennej targetToDestroy jest cokolwiek przyporządkowane i ciągle jest zwrócona na północ (mimo że cel znajduje się z boku. Czego nie widzę?

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

public class HoverTank_CommandSet : MonoBehaviour {


	public GameObject EventSystem;

	//public GameObject Body;

	public GameObject turret;
	public float turretRotSpeed = 10f;

	public GameObject targetPointer;

	private bool isAttacking = false;

	private GameObject targetToDestroy;

	private Vector3 targetPointerPosition;

	private Quaternion targetPointerRotation, turretRotation, originTurretRotation;

	[HideInInspector]
	public float range;

	void Start() {
		range = this.gameObject.GetComponent<ObjectStats> ().attackRange;

		turretRotation = turret.transform.rotation;
		originTurretRotation = turret.transform.rotation;

		targetPointerPosition = targetPointer.transform.position;
		targetPointerRotation = targetPointer.transform.rotation;
	}

	void Update () {

		if (this.gameObject.GetComponent<SelectionComponent> ().currentlySelected == true) {
			
			if (Input.GetMouseButtonUp (0)) {

				RaycastHit hitClick;

				if (Physics.Raycast (Camera.main.ScreenPointToRay (Input.mousePosition), out hitClick)) {
					//jeśli nie klikniesz w teren
					if (hitClick.collider.gameObject.layer != 0) {

						//jeśli klikniesz na wroga
						if (hitClick.collider.gameObject.GetComponent<ObjectStats> ().isEnemy == true) {

							isAttacking = true; //zmień status na "atakuję"
							targetToDestroy = hitClick.collider.gameObject; //zdefiniuj co jest wrogiem
						}
					}
				}
			}
		}

		//jeśli jest wyznaczony cel do zniszczenia
		if (targetToDestroy != null) {
			LocateYourTarget (targetToDestroy); //znajdź taką rotację wieży, która będzie na linii ognia
		}

		//jeśli rotacja wieży będzie większa niż 180 stopni w prawo, bardziej opłaca się obrócić w lewo
		//poprawka wprowadzona dla pomocniczego targetPointera
		if (targetPointerRotation.y - originTurretRotation.y > 180) {
			targetPointerRotation.y -= 360;
			targetPointer.transform.rotation = Quaternion.RotateTowards (targetPointer.transform.rotation, targetPointerRotation, 360*Time.deltaTime);
		}


		if (isAttacking == true) {
			
			if (turretRotation != targetPointerRotation){
				turret.transform.rotation = Quaternion.RotateTowards (originTurretRotation, targetPointerRotation, turretRotSpeed * Time.deltaTime);
			}

			//liczy odległość od jednostki do wskazanego celu do zniszczenia
			float deltaX = targetPointerPosition.x - targetToDestroy.transform.position.x;
			float deltaZ = targetPointerPosition.z - targetToDestroy.transform.position.z;
			float distance = Mathf.Sqrt (deltaX * deltaX + deltaZ * deltaZ); //Pitagoras

			//jeśli wróg jest w zasięgu, obniżaj HP przeciwnika
			if (distance <= range) {

				targetToDestroy.GetComponent<ObjectStats> ().HP -= 1 * Time.deltaTime;

				if (targetToDestroy.GetComponent<ObjectStats> ().HP <= 0) {
					targetToDestroy = null;
					isAttacking = false;
				}
			}

			//jeśli nie to będę dopracowywał później
			else {
				
			}
		}
	}

	public void LocateYourTarget(GameObject Enemy){

		RaycastHit targetYourEnemy;

		if (Physics.Raycast(targetPointerPosition, targetPointer.transform.forward, out targetYourEnemy, range)){
			
			if (targetYourEnemy.collider.gameObject != Enemy) {
				
				for (int x = 0; x < 360; x++){

					RaycastHit helperHit = new RaycastHit ();

					//czy w każdym cyklu pętli trzeba "aktualizować" Raycasta, czy ulega on rotacji tak samo jak TargetPointer i taki warunek jest zbędny
					if (Physics.Raycast(targetPointerPosition, targetPointer.transform.forward, out helperHit, range)){

						if (helperHit.collider.gameObject != Enemy) {

							//czy ja dodaję w stopniach czy w radianach
							targetPointerRotation.y += 1;
							targetPointer.transform.rotation = Quaternion.RotateTowards (targetPointer.transform.rotation, targetPointerRotation, 500);
						}
					}
				}
			}
		}
	}
}

#2

Hmm jakoś za bardzo to skomplikowałeś.
Zacznij od napisania skryptu który odwróci twoją lufę do wroga. Tutaj masz kawałek który wykorzystuje w targetIndicator do wyznaczania kierunku do celu:

 float GetAngle(GameObject target, GameObject movable)
{
    float angle;
    float xDiff = target.transform.position.x - movable.transform.position.x;
    float zDiff = target.transform.position.z - movable.transform.position.z;

    angle = Mathf.Atan(xDiff / zDiff) * 180f / Mathf.PI;
    // tangent only returns an angle from -90 to +90.  we need to check if its behind us and adjust.
    if (zDiff < 0)
    {
        if (xDiff >= 0)
            angle += 180f;
        else
            angle -= 180f;
    }
   

    // this is our angle of rotation from 0->360
    float playerAngle = movable.transform.eulerAngles.y;
    

    // now subtract the player angle to get our relative angle to target.
    angle -= playerAngle;
    return angle;
}

Zrezygnowałbym też z Raycasta na rzecz matematycznego wyznaczania kierunku, edytując powyższa metodę uzyskasz naprowadzanie w góre/dół dla działka. Jeśli będziesz miał to w osobnym skrypcie to w razie potrzeby dasz enabled = true i zacznie sam naprowadzać po wrzuceniu celu. Pisz kod w postaci mniejszych, niezależnych kawałków to będzie ci łatwiej nad wszystkim zapanować. Zwłaszcza przy pisaniu AI musisz o tym pamiętać skoro nie chcesz używać maszyny stanów czy drzewek behawioralnych bo tutaj łatwo o zawał gdy dodajesz nowe reakcje/stany do AI:smile:


#3

Ciekawe, faktycznie wygląda znacznie prościej, muszę to dzisiaj wypróbować :slight_smile:
Tamten mój skrypt to była wersja robocza, po zmuszeniu go do działania wiele rzeczy bym wywalił do innego skryptu - w końcu jak widzisz to miał być zbiór komend i czynności dla jednostki danego typu, a że nie ma sensu pisać ciągle tych samych wyrażeń dla każdej z jednostki z osobna, miałem zamiar napisać skrypt z wyrażeniami uniwersalnymi, do których później każdy CommandSet by się odwoływał.


#4

Hmm ogólnie dobrą zasada jest pisanie jednozadaniowych komponentów, na twoim miejscu utworzyłbym osobne skrypty do:

  • poruszania do celu
  • celowania do wroga
  • zachowanie behawioralne: czuwanie, podążanie za celem, atak celu -> w momencie w którym ilość stanów by się zwiększała zrobiłbym prostą maszynę stanów czyli każde zachowanie Atak, czuwanie, podążanie dał jako osobny komponent i dynamicznie je usuwał za pomocą Destroy(oldStateComponent) i dodawał AddComponent() w momencie ich przejścia.

#5

Zastosowałem skrypt @Radomiej 'a i wprowadziłem zmiany. Najbardziej bolesnym błędem okazało się korzystanie z Quaternionów zamiast Vector3 przy zmianie rotacji. Jestem znacznie bliżej rozwiązania… no właśnie, dalej jest problem.
Wieża się obraca do właściwej pozycji, i prawdopodobnie (zaraz wyjaśnię czemu nie na pewno) non stop aktualizuje pozycję wieżyczki w czasie ruchu jednostki, obstawiam że także gdyby to przeciwnik się poruszał. Trudno mi to jednak ocenić, bo wieżyczka… świruje. Gdy tylko się zaczyna ruszać, buja się na wszystkie strony, wokół wszystkich możliwych osi, na sam koniec kierując lufę w swój własny korpus w dół… i rotując radośnie w kółko. Żeby nie było już żadnych wątpliwości, stworzyłem pusty obiekt SLOT_Turret, do którego podczepiłem Turreta oraz SLOT_MainBody, do którego podczepiłem resztę (oba Sloty są podczepione do obiektu HoverTank i mają taką samą rotację oraz orientację).
Skrypt LocateEnemy zawiera funkcję w takiej postaci jak został zaproponowany wcześniej, bez żadnych modyfikacji. Odwołania do skryptu jednostki ze statystykami działają prawidłowo, status isAttacking też jest dobrze zmieniany. Co przeoczyłem?

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

public class HoverTankCommandSet : MonoBehaviour {

	public GameObject AI;
	public GameObject mainBody;
	public GameObject turret;

	private GameObject targetToDestroy;

	private Vector3 unitRot;
	private Vector3 turretOriginRot;
	private Vector3 turretPresentRot;

	private float range;
	private float turretRotSpeed;
	private float angleRot;

	void Start () {
		unitRot = mainBody.transform.eulerAngles;
		turretPresentRot = turretOriginRot;
		range = this.gameObject.GetComponent<ObjectStats> ().attackRange;
		turretRotSpeed = this.gameObject.GetComponent<ObjectStats> ().turretRotSpeed;
	}
	
	void Update () {
		turretOriginRot = this.gameObject.transform.eulerAngles;

		if (this.gameObject.GetComponent<SelectionComponent> ().currentlySelected == true) {

			if (Input.GetMouseButton (0)) {
				
				RaycastHit hitClick;

				if (Physics.Raycast (Camera.main.ScreenPointToRay (Input.mousePosition), out hitClick)) {
					//jeśli nie klikniesz w teren
					if (hitClick.collider.gameObject.layer != 0) {

						targetToDestroy = hitClick.collider.gameObject;

						//jeśli klikniesz na wroga
						if (targetToDestroy.GetComponent<ObjectStats> ().isEnemy == true) {
							//zmień status na "atakuję"
							this.gameObject.GetComponent<ObjectStats>().isAttacking = true;
						}
					}
				}
			}
		}
		//jeśli ma status Atakuję a tym samym ma wyznaczony cel do zniszczenia, wyznacza kąt obrotu wieży - aktualizowane co klatkę
		if (this.gameObject.GetComponent<ObjectStats>().isAttacking == true) {
			angleRot = AI.GetComponent<LocateEnemy> ().GetAngle (targetToDestroy, turret);
			turretPresentRot.y += angleRot;
			turret.transform.eulerAngles = Vector3.RotateTowards (turret.transform.eulerAngles, turretPresentRot , turretRotSpeed * Time.deltaTime, 0);
		}
		// a jeśli ma przeciwny status, to obraca wieżę do rotacji standardowej (takiej samej jak sam czołg), by lufa była skierowana do przodu
		else {
			if (turretPresentRot != turretOriginRot) {
				turret.transform.eulerAngles = Vector3.RotateTowards (turret.transform.eulerAngles, turretOriginRot , turretRotSpeed * Time.deltaTime, 0);
			}
		}
	}
}

#6

Zobacz czy jak zmienisz na lokalną rotacje wieżyczki nie będzie lepiej, bo zapomniałem napisać że to jest wersja na obracanie dla lokalnego obiektu, na dole dałem ci ogólny skrypt na obracanie który obróci to poprawnie dla normalnej rotacji.

public class TargetIndicator : MonoBehaviour
{
public Transform movable;
public Transform target;
public float visibleRange = 2;

private SpriteRenderer spriteRenderer;
// Use this for initialization
void Start()
{
    spriteRenderer = GetComponentInChildren<SpriteRenderer>();
}

// Update is called once per frame
void Update()
{
    if (target == null)
    {
        spriteRenderer.enabled = false;
        return;
    }
    float angle = GetAngle(target.gameObject, movable.gameObject);
    //Debug.Log("Angle: " + angle);
    RaTools.RotateLocalY(transform, angle);
    if(RaTools.DistanceFlat(target, movable) < visibleRange)
    {
        spriteRenderer.enabled = false;
    }
    else
    {
        spriteRenderer.enabled = true;
    }
}

float GetAngle(GameObject target, GameObject movable)
{
    float angle;
    float xDiff = target.transform.position.x - movable.transform.position.x;
    float zDiff = target.transform.position.z - movable.transform.position.z;

    angle = Mathf.Atan(xDiff / zDiff) * 180f / Mathf.PI;
    // tangent only returns an angle from -90 to +90.  we need to check if its behind us and adjust.
    if (zDiff < 0)
    {
        if (xDiff >= 0)
            angle += 180f;
        else
            angle -= 180f;
    }
   

    // this is our angle of rotation from 0->360
    float playerAngle = movable.transform.eulerAngles.y;
    

    // now subtract the player angle to get our relative angle to target.
    angle -= playerAngle;
    return angle;
}

}

Podam ci jeszcze inny skrypt do obliczenia obrotu:

    if (target == null) return;     

    targetPoint = new Vector3(target.transform.position.x, transform.position.y, target.transform.position.z) - transform.position;
    targetRotation = Quaternion.LookRotation(targetPoint, Vector3.up);
    transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * rotationSpeed);

gdzie:

GameObject target = <Twój cel>
float rotationSpeed = 5

zmieniając wartości Vector3.up, Vector3.forward, Vector3,right powinieneś sobie ustawić osie dla których obrót działa.


#7

No i się okazało że rozwiązanie jest prostsze niż by się wydawało - wszystko rozjaśnił tutorial https://www.youtube.com/watch?v=QKhn2kl9_8I

Po pierwsze: można potraktować cel i wieżyczkę jako Transform, a nie GameObject.
Po drugie - trzeba umiejętnie żonglować EulerAngles oraz Quaterniony. Wygodniejsze i bardziej zrozumiałe są EulerAngles (należące do Vector3) i to na nich trzeba stosować modyfikacje, ale w pewnych momentach trzeba je konwertować do Quaternionów. Co więcej, wyznaczanie kąta obrotu jest zbędne, bo od tego też jest wbudowana funkcja. Mało tego, ten ruch może być płynny! Oto “kontrowersyjny” fragment skryptu, który bazuje na powyższym tutku i teraz DZIAŁA idealnie:

	if (this.gameObject.GetComponent<ObjectStats>().isAttacking == true) {
		Vector3 dir = targetToDestroy.transform.position - turret.transform.position;
		Quaternion lookRotation = Quaternion.LookRotation (dir);
		Vector3 rotation = Quaternion.Lerp(turret.rotation, lookRotation, turretRotSpeed*Time.deltaTime/10).eulerAngles;
		turret.rotation = Quaternion.Euler (0f, rotation.y, 0f);
	}
	else {
		Vector3 rotation = Quaternion.Lerp(turret.rotation, mainBody.rotation, turretRotSpeed*Time.deltaTime/10).eulerAngles;
		turret.rotation = Quaternion.Euler (0f, rotation.y, 0f);