[Unity 5 C#] Problem z czyszczeniem listy


#1

Mam spory problem ze skryptem, może moglibyście by mi pomóc.

Mam skrypt SelectionManager.cs (zamieszczony na samym dole posta). Konkretnie problemem jest metoda DeselectAll(). Nie dostaję żadnych błędów, ale w Playmode widzę, że mimo odznaczenia jednostek do nowego punktu docelowego przemieszczają się wszystkie, nawet jeśli po odznaczeniu zaznaczę zupełnie inne. Tak jakby wszystkie jednostki, które zostały dodane do listy selectedObjects, nie mogły zostać stamtąd wyrzucone. Czyli linijka selectedObjects.Clear() z jakiegoś powodu jest całkowicie ignorowana. Ok, myślę sobie. W takim razie usunąłem tę linijkę i wstawiłem do pętli foreach selectedObjects.remove (obj). W playmode za każdym razem dostaję error (z pauzą, bo tak sobie ustawiłem żeby wiedzieć, gdzie dokładnie problem występuje), który brzmi:

InvalidOperationException: Collection was modified; enumeration operation may not execute. System.Collections.Generic.List` 1+Enumerator[UnityEngine.GameObject].VerifyState() (at /…/List.cs:778)

Zacząłem pytać na forum.unity.com i tam mi dwie osoby wyjaśniły, że nie mogę w ten sposób modyfikować tej listy, a jeśli chcę ją “wyczyścić”, powinienem zamiast selectedObjects.Clear() użyć selectedObjects = new List(). Jeszcze nie dostałem odpowiedzi, ale to też nie działa. Spróbowałem “na chama” wstawić obie linijki, najpierw z new, pod spodem z Clear. Co się okazało? (dla uproszczenia zawężam problem do dwóch kostek)

  1. Jeśli zaznaczę A i/lub B, odznaczę i kliknę by wyznaczyć cel - nic się nie dzieje, A i B zostaje na miejscu - DOBRZE
  2. Jeśli zaznaczę kostkę A, odznaczę ją, zaznaczę B i wyznaczę cel - tylko B się tam przesuwa - DOBRZE.
  3. Jeśli zaznaczę kostkę A, wyznaczę jej cel, odznaczę, zaznaczę B i wyznaczę nowy cel - oba się przesuwają do drugiego celu - BARDZO ŹLE.

Coś mi umknęło? Jak to ominąć?

Obiecany kod:

[CODE]using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SelectionManager : MonoBehaviour {

public Transform target;

[HideInInspector]
private Vector3 startSquare;

[HideInInspector]
private Vector3 endSquare;

[SerializeField]
public LayerMask selectablesLayer;

[HideInInspector]
public List<GameObject> selectedObjects;

[HideInInspector]
public List<GameObject> selectableObjects;

private float widthSquare;
private float heightSquare;

void Awake (){
	selectedObjects = new List<GameObject> ();
	selectableObjects = new List<GameObject> ();
}

void Start () {
}

void Update () {

	if (Input.GetMouseButtonDown (0)) {
		startSquare = Camera.main.ScreenToViewportPoint (Input.mousePosition);
	}

	if (Input.GetMouseButtonUp (0)) {

		endSquare = Camera.main.ScreenToViewportPoint (Input.mousePosition);
		if (endSquare != startSquare) {
			SelectInArea ();
		}

		else {
			SelectMe ();
		}
			

	}

	if (Input.GetMouseButtonUp (1)) {
		DeselectAll ();
	}
}

void SelectMe (){

	RaycastHit hit;

	if (Physics.Raycast (Camera.main.ScreenPointToRay (Input.mousePosition), out hit, selectablesLayer)) {

		if (hit.collider.gameObject.layer == 8) {

			if (!(Input.GetKey (KeyCode.LeftShift))) {
				DeselectAll ();
			}

			SelectionComponent clickOnScript = hit.collider.GetComponent<SelectionComponent> ();

			if (clickOnScript.currentlySelected == false) {

				selectedObjects.Add (hit.collider.gameObject);
				clickOnScript.currentlySelected = true;
				clickOnScript.SelectMe ();
			}
		}

		else {
			MoveToTarget ();
		}
	}
}

void SelectInArea(){

	widthSquare = endSquare.x - startSquare.x;
	heightSquare = endSquare.y - startSquare.y;

	if (!(Input.GetKey (KeyCode.LeftShift)) && Mathf.Abs (widthSquare) > 0.02 && Mathf.Abs (heightSquare) > 0.02) {
		DeselectAll ();
	}

	Rect selectSquare = new Rect (startSquare.x, startSquare.y, widthSquare, heightSquare);

	foreach (GameObject selectObject in selectableObjects) {

		if (selectSquare.Contains (Camera.main.WorldToViewportPoint (selectObject.transform.position), true)) {
			selectedObjects.Add (selectObject);
			selectObject.GetComponent<SelectionComponent> ().currentlySelected = true;
			selectObject.GetComponent<SelectionComponent> ().SelectMe ();
		}

	}



}

void DeselectAll() {

	foreach (GameObject obj in selectedObjects) {
		obj.GetComponent<SelectionComponent> ().currentlySelected = false;
		obj.GetComponent<SelectionComponent> ().SelectMe();
	}

	selectedObjects.Clear ();
}

void MoveToTarget (){

	RaycastHit targetPoint;
	Vector3 targetPosition;

	if (Physics.Raycast (Camera.main.ScreenPointToRay (Input.mousePosition), out targetPoint, Mathf.Infinity)) {

		targetPosition.x = targetPoint.point.x;
		targetPosition.y = -10f;
		targetPosition.z = targetPoint.point.z;

		foreach (GameObject obj in selectedObjects) {
			Instantiate (target);
			target.transform.position = Vector3.MoveTowards(transform.position, targetPosition, Mathf.Infinity);

			AIPath aiPathScript = obj.GetComponent<AIPath> ();
			aiPathScript.target = target;

			DynamicGridObstacleAssistant unitObstacleScript = obj.GetComponent<DynamicGridObstacleAssistant> ();
			unitObstacleScript.isMoving = true;
		}
	}



}

}

[/CODE]

Od razu mówię, że mam świadomość faktu, że po wyznaczeniu nowego celu stary obiekt “pozostaje” - jak się uporam z przedstawionym problemem, zamierzam tę funkcję dodać.


#2

nie wiem czy dobrze mysle, ale wydaje mi się że problem tkwi nie w selectedobjects, a raczej w MoveToTarget. Chyba operujesz cały czas tym samym obiektem target, czyli (punkt 3) jak wyznaczysz cel dla A, odznaczysz i wyznaczasz cel dla B, to napisujesz wartość celu dla A i oba idą w to samo miejsce.
Spróbuj czy zadziała jak zamiast

Instantiate (target);

dasz

GameObject t = Instantiate(target) as GameObject;
t.transform.position = … i tak dalej

…ale pamiętaj że jestem amatorem i mogę się mylić :slight_smile:


#3

Sprawdziłem i lipa :confused: Jedyna różnica w stosunku do tego, co napisałeś, to nie dałem GameObject t … blabla tylko Transform (no bo target to Transform a nie GameObject). No i zaczęła się zabawa, bo z jakiegoś powodu każdy obiekt dostaje ni z gruszki ni z pietruszki DWA targety - jeden tam gdzie kliknąłem, drugi gdzieś z boku (nie mam pojęcia jak i dlaczego się tworzy), za każdym razem w tym samym punkcie. Oczywiście jednostka jedzie do tego drugiego punktu, to samo z pozostałymi. W efekcie wszystkie tłoczą się w jednym punkcie, którego na dodatek nie wybierałem :hushed:
Jakiś ziomek z forum.gamedev poradził mi, żebym wywalił przypisywanie target z SelectionManager i wrzucił do SelectionComponent, a w pętli foreach wywoływał tylko metodę z SelectionComponent. Oczywiście nic to nie dało. :rage:

Skoro jest taki duży problem, może ktoś bieglejszy ode mnie przyjmie paczuszkę ze sceną i używanymi skryptami, żeby sprawdzić, co jest grane? :anguished:


#4

Jednak intuicja obsesa nie zawiodła - lista jest czyszczona prawidłowo (sprawdzone Debug.Logami), problem jest z przypisywaniem nowych targetów. Niestety jest to chyba nie do rozwiązania, więc będę musiał zrealizować to inaczej (na szczęście wiem już jak) :slight_smile:


#5

Jest noc więc nie wiem czy dobrze rozumiem co chesz zrobić i mogę pisać niezrozumiale.
Instantiate (target); nigdzie nie masz referencji do obiektu który tworzysz, czyli za każdym kliknięciem tworzysz obiekt ktorego nie używasz i nie masz do niego żadnego odniesienia.
Tak jak Obses podał - użyj GameObject bo Transform i tak jest zawsze podłączony do jakiegos obiektu i sam z siebie nie istnieje.

Nie wiem czy dobrze rozumiem ale tworzysz nowy target i przenosisz go do punktu klikniecia? Jeżeli tak to twój kod tworzy nowy obiekt target ale przenosi cały czas jeden i ten sam który masz na początku skryptów.
Nie widzę całego kodu ale transform target jest wogóle nie potrzebny?

target w skrypcie AIPath jest typu transform? I co dalej robi z tym “targetem” bo teraz tak - jak przypiszesz swoj transform target do transform w AiPath -> to jak pierwszy transform zmieni pozycje to ten w AiPath tez sie zmieni - bo nie przesyłasz wartości transformu ale sam transform :] tak jakbyś dodał w edytorze.
Czyli jak w AiPath robisz coś z target -> to on bierze wartości z transform który przypisales czyli ten w powyższym skrypcie. czyli każdy target w każdym AiPath zawsze “patrzy” na ten sam target w powyzszym skrypcie.
Jak wstanę rano i nadal nie załapiesz to dodaj skrypt AiPath a ja ci to rozpiszę.
Możesz też poskracać kod + mniej zmiennych. np:
vector3 targetPosition = new Vector3(targetPoint.point.x, -10.0f, targetPoint.point.z);
target już jest transformersem więc target.position = targetPosition;

Chyba dobrze myślę, jak nie to sory za chaos ale już praktycznie śpię :smiley:


#6

@HokanPL

Własnie nie może być Transform tylko GameObject, dlatego że transform sam w sobie nie może istnieć, zawsze musi być elementem jakiegoś GameObjectu - u Ciebie gdy pisałeś Transform on się gdzieś pod jakiś GameObject podpinał, i pewnie z tego GameObjectu czerpał dane o pozycji, i własnie tam szły jednostki. Jeśli się potem odwołujesz do transform (bo Twój target to transform), to dalej używasz t.transform.position = … itd.

Też nie widze całości, ale moje rozumowanie jest takie, że Transform target to prefab którego używasz jako wskaźnika gdzie mają isc jednostki. Więc wydaje mi się, że generując je powstanie kilka targetów i jednostki będą do nich szły.


#7

Nie rozumiem trochę założeń poruszania. Fragment metody MoveToTarget()

//nie wiem czy można spawnować samo Transform, ale jak tak to ten "target" z góry przenosisz w pętle
//--
Transform target = new Transform();//nie wiem czy to tak działa, nie mam teraz pod ręką Unity, a zrobił bym to prędzej z Vector3
//--
Instantiate (target);
//to wywala target gdzieś w nieskończoność, i w sumie nie target tylko kierunek w którym znajduje się cel.
target.transform.position = Vector3.MoveTowards(transform.position, targetPosition, Mathf.Infinity);
AIPath aiPathScript = obj.GetComponent<AIPath> ();
//po co wysyłać cały transform jak można wysłać samą pozycje Vector3, a w ten sposób przypisujesz referencje zamiast nowych danych dlatego każdy idzie w "tym samym kierunku", a raczej ostatniego w pętli
aiPathScript.target = target;

A najlepiej było by poprawić powyższy kod do czegoś takiego.

obj.GetComponent<AIPath>().target = targetPosition;

#8

tylko że pewnie target w AiPAth to on ma jako transform… dlatego chcialem zobaczyc co on ma tam napisane.
bo jak jest transform to bardziej target.position = targetPosition; a jak jest V3 to tylko target.

Można by to udoskonalic zeby sie poruszalo do ruchomych celów -> wtedy z malymi modyfikacjami by sie zgadzalo;
jak cel ma transform -> podpiac sie pod niego, jezeli nie to instantiate nowy transform (tylko trzymac jakas referencje do niego zeby mozna bylo go pozniej zniszczyc) i umiescic na celu (w przypadku np klikniecia na ziemi). Ewentualnei funkcja w aipath przeladowana (Transform target) i (V3 target) i w zaleznosci inaczej reagowac. to wtedy kazdy by sie ruszal do swojego celu nawet jakby ten target sie ruszal. Moze to chcial osiagnac.
A jak tylko ruch do wskazanego statycznego punktu -> no to v3 klikniecia przypisac do celu i po sprawie.


#9

Dobra, panie i panowie, problem rozwiązany. Próbowałem w AIPath (Pathfinding Arona Granberga w wersji free) zmienić transform na GameObject i u siebie też, ale wtedy cały system się wysypuje. Gra nie warta świeczek.

Po prostu na każdą jednostkę przypada teraz gotowy GameObject target o takiej samej pozycji jak dana jednostka (pozycja Y obniżona by target zawsze był pod terenem). Po prostu muszę pamiętać później, że po zaimplementowaniu rekrutacji jednostek (dłuuga droga jeszcze) oprócz jednostki tworzył się niepodpięty do niej (w sensie nie w hierarchii) target i był on przypisywany do skryptu tej jednostki.

Ale dzięki Waszym komentarzom trochę się nauczyłem i uporządkowałem skrypt. Dzięki! :slight_smile:


#10

Nie wiem czemu upierasz się żeby mieć obiekt z transformersem? (mogłbys to wyjaśnić? może po coś specjalnego) Jak gdzieś klikniesz to masz współżędne -> v3 i tam idzie/jedzie twoja jednostka po dodaniu do target (musi być odpowiendni typ zmiennej, albo funkcja która to zmienia)
Chyba że chesz żeby można było np iść/jechać/atakować poruszający się obiekt -> to wystarczy jako target dać transform takiego obiektu-celu. Bez zbędnego tworzenia nowych czy to na początku czy w run-time.
A jak już musisz mieć te obiekty,to zamiast tworzyć te targety do każdej jednostki (instantiate albo na twardo dodając) -> zrób sobie np pooling obiektów(object pooling) na starcie 10 pewnie wystarczy (pewnie i tak za dużo) - minus tego taki że musisz kontrolować czy dany target jest gdzieś przypisany - i jak nie do zwolnić do poola. jak dasz inne rozkazy albo np dojadą do celu.
Najprościej trzymać współrzędne celu w zmiennej typu vector3 na aipath np albo na poruszanym obiekcie i tyle.

Dobrym zwyczajem jest trzymanie wrażliwych zmiennych (tak jak twój target na aipath) jako private z get{} set{} albo własną funkcją obsługującą.
Tak w skrócie :smiley: Staraj się przeglądać kod i próbować go zrozumieć zamiast losowo zmieniać rzeczy :stuck_out_tongue:
To tak odemnie - ostrzegam nie jestem ekspertem i mogę się mylić.


#11

Tylko dlatego używam Transforma, ponieważ skrypt AIPath (nie mój, zewnętrzny asset) go używa dla obiektu Target. Spróbowałem zmienić na GameObject - cały system pathfindingu się wysypał. :stuck_out_tongue: A gdy próbowałem w swoim skrypcie zmienić na GameObject, a w AIPath zostawić Transform, to wyskakiwał mi błąd (że nie da się przekonwertować GameObjecta do Transforma)


#12

Mozesz mi wyslac link do tego AiPath? Chcialbym go zobaczyc.
Czy gra ktora robisz to jakis RTS?

Pamietaj ze przypisujac transform do czegps to przypisujesz ten wlasnie transform(jakby link do niego w skrocie) a nie jego wartosc.


#13

Łap:

https://arongranberg.com/astar

Wersja free jest trochę okrojona w stosunku do płatnej, ale i tak jest w porządku. Darmówkę można nawet wykorzystywać nieodpłatnie w projektach komercyjnych, a wsparcie pieniężne w takim przypadku jest “mile widziane, ale niekonieczne”. Pytałem na forum o szczegóły licencji, bo teraz mój projekt - tak, RTS - robię w pojedynkę i z pasji, nie nastawiając się na komercjalizację. Ale wolałem się dopytać, bo kto wie, może coś poważniejszego z tego wyrośnie w dalekiej przyszłości.


#14

Dopiero dzisiaj widzę twoją odpowiedź, nie wiem czemu przez pół miesiąca nie widziałem nowych postów i myślałem że forum jest martwe :smiley: Dopiero dzisiaj się wszystko odblokowało… dziwne.
Dzisiaj popatrzę na ten skrypt - ale problem jest raczej w tym co pisałem.
Chyba że już ogarnąłeś bo dużo czasu mineło.


#15

Dzięki, ogarnąłem - ostatecznie 3 skrypty zawierają odwołanie do targeta - AIPath traktuje go jako transforma, a moje 2 skrypty (zwłaszcza ten zawierający pętlę) drobnych korektach zakładają że jest to zwykły GameObject. Zrezygnowałem też ze stosowania Intstantiate - zamiast tego każdy obiekt ma przyporządkowany EmptyObject, który spełnia funkcję targeta. Gdy jednostka się nie rusza (isMoving=false), pozycja targeta jest taka sama jak jednostki, a skrypt pathfindingu jest dla tej jednostki wyłączony. Przy zdefiniowaniu nowego celu, isMoving = true, pathfinding się odpala, a jednostka zaczyna się przemieszczać tam, gdzie powinna.

Obecnie jest dopracowywany (choć nie tylko to) system dawania celów, gdy kilka jednostek jest zaznaczonych (by nie przepychały się w jednym punkcie, tylko grzecznie stawały obok siebie) - przy okazji wyszedł mi system formacji :smiley: