Logika potworów w grze RPG

0

Witam. Napisałem prostą logikę potworów w grze. Wydaje mi się, że przewidziałem każde zachowanie. Co o tym sądzicie ?

Logika ma działać następująco:
-gdy gracz jest w zasiegu ataku - potwór atakuje gracza
-gdy gracz jest w polu widzenia - potwór idzie na pozycje gracza
-gdy gracz znika z pola widzenia - potwor idzie do miejsca, w którym ostatnio widział gracza
-gdy potwór dojdzie do celu i nie ma w zasięgu ataku ani w polu widzenia gracza to nic nie robi
-gdy potwór nic nie robi to co jakiś czas szuka sobie celu do którego może się przejść

Bazowe funkcje to:
-playerInAttackRange(); // bool gracz w zasięgu ataku
-playerInViewRange(); // bool gracz w polu widzenia
-player->takeDamage(2); // zadaj "2" obrazenia graczowi
-searchPath(); // szuka drogi do celu opisanej punktami

Stany:
-state::fight; // stan atakowania gracza
-state::run; // stan dazenia do jakiegoś celu
-state::idle; // potwor stoi bezczynnie w miejscu

Dodatkowo dodałem:
-sf::vector2f pointBase; // punkt spawnu potwora
-sf::vector2f target; // cel do ktorego potwor zmierza
-std::vector <Point> path; // droga do celu opisana punktami ( powinna być opisywana przez punkty sf::Vector2f ale jeszcze tego nie zrobiłem :-/ )

if (state == states::fight) {

	if (playerInAttackRange()) {
		
		if (cooldown <= 0) {
			player->takeDamage(2);
			cooldown = attackTime;
			frame = 0;
		}

		frame = cooldown / attackTime * 4.0f - 1.0f;
		sprite->setTexture(*fightTextures[direction * 4 + frame]);
		
	}
	else if (playerInViewRange()) {
		target = player->position;
		state = states::run;
	}
	else
	{
		state = states::idle;
		frame = 0;
		sprite->setTexture(*idleTextures[direction * 4 + frame]);
	}
}
else if (state == states::run) {
	
	if (playerInAttackRange()) {
		state = states::fight;
	}
	
	if (playerInViewRange()) {
		target = player->position;
	}

	calculateCurrentFrame(dt);
	float distance = 15.0f * stepSize * dt;
	move(distance);
	sprite->setTexture(*runTextures[direction * 4 + frame]);
}

else if (playerInAttackRange()) {
	state = states::fight;
}
else if (playerInViewRange()) {
	target = player->position;
	state = states::run;
}

if (state == states::idle) {
	if (rand() % 300 == 0) {

		// rangdom target
		target.x = pointBase.x + (rand() % 3 - 1) * 100;
		target.y = pointBase.y + (rand() % 3 - 1) * 100;
		state = states::run;
	}
	else {
		calculateCurrentFrame(dt);
		sprite->setTexture(*idleTextures[direction * 4 + frame]);
	}
}
0

Zamiast wrzucać kod opisz co ona robi.

4

Super. 10/10. najbardziej podoba mi się fragment if (rand() % 300 == 0). Kiedy można spodziewać się całej gry?

1

Jeśli o mnie chodzi, to jest dobrze — ot prosta maszyna stanów, robi to co do niej należy. Przy większej liczbie możliwych stanów, zamiast drabinki ifów, zastanów się nad zastosowaniem instrukcji wyboru. Ale póki co jest w porządku, choć fajnie by było zobaczyć ten kod w akcji, czyli gdybyś nagrał krótkie wideo ilustrujące jego działanie.

KamilAdam napisał(a):

najbardziej podoba mi się fragment if (rand() % 300 == 0).

Mi też — wygląda jak easter egg, mocno nieprawdopodobny, odpalany w losowych momencie. 😉

Choć zmieniłbym ten warunek, dlatego że jego działanie jest zbyt nieprzewidywalne. Zamiast w każdej klatce robić rand, zadeklarowałbym osobny licznik dla stanu idle i w trakcie aktywacji tego stanu, wylosowałbym wartość licznika raz, a następnie w każdej klatce go inkrementował, a po doliczeniu do końca, aktywował nowy stan (tutaj jest to run w losowym kierunku).

Chodzi o to, aby po pierwsze zwiększyć prawdopodobieństwo wykonania tej automatycznie odpalanej akcji, a po drugie, aby móc ustalić konkretny przedział czasu jej zaistnienia. Żeby się nie kazało, że ustawia się stan idle, a już w następnej klatce ta dodatkowa akcja zostanie wykonana (bo rand akurat zwrócił pasujące ziarno). Czyli coś w tym stylu:

counter = 300 + rand() % 300;

Akcja zostanie wykonana w losowym momencie, ale nie wcześniej niż po pięciu sekundach i jednocześnie nie później niż po dziesięciu sekundach. Oczywiście zakładając, że logika aktualizowana jest 60 razy na sekundę, a literały 300 dotyczą liczby klatek, a nie delty.

W ten sposób, czyli losując przedziały czasu, od zarania dziejów implementowało się specjalne akcje idle. Wystarczyło pozostawić główną postać, nie ruszać nią, a po X sekundach ta zaczynała robić jakąś głupotę — Earthworm Jim robił śmieszne rzeczy.

1

Ewentualnie jakbyś chciał to jeszcze możesz zrobić tak, że jest klasa bazowa State, i każdy stan to osobna implementacja. Masz kilka klas, każda ładnie sobie implementuje co tam w środku robi. Wtedy też już nie trzeba if/switch, tylko wołasz, dajmy ".Update(dt)" na aktualnym stanie.

1

W przypadku, gdy dany obiekt może posiadać jeden z kilku stanów, implementacja maszyny stanów nie ma zbyt dużego znaczenia. Kodu jest tak mało, że można go zamknąć w jednej funkcji (jak to widać w pierwszym poście).

Przy większej liczbie stanów, switch jest lepszy, bo lepiej w nim widać poszczególne fragmenty obsługi danych stanów i łatwiej pomiędzy nimi skakać. Niektórzy preferują nawet i gigantyczne switche, na setki linijek kodu, dlatego że cała maszyna jest w jednej funkcji, łatwo się to analizuje i debuguje, ale też łatwo jest przeskakiwać pomiędzy stanami w ramach jednego wywołania funkcji aktualizującej.

Przy setkach możliwych stanów danego obiektu, taki switch miałby pewnie tysiące linijek kodu i zrozumienie jak działa, dla wielu było by po prostu zbyt trudne. Podział na osobne implementacje stanów mógłby wszystko ułatwić. Pod warunkiem oczywiście, że nie stałaby się jakąś gigantyczną, wielopoziomową abstrakcją, której zrozumienie i debugowanie byłoby koszmarem.

2
player->takeDamage(2);

To jest problematyczne, że to potwór ustala, z jaką siłą gracz dostanie zniszczenie.
A co jeśli gracz będzie miał jakiś bonus, który sprawi, że będzie bardziej odporny na atak potwora? (np. będzie miał zbroję).

Myślę, że tu potrzebna jest jakaś abstrakcja.

  • Potwór atakuje z jakąś siłą
  • Gracz przyjmuje atak i może być mniej lub bardziej odporny na niego

To jakby dwa etapy. Mogłoby to być rozwiązane jeszcze w innym miejscu, zewnątrz, w jakimś systemie walki.

sprite->setTexture(*fightTextures[direction * 4 + frame]);

To w ogóle wywaliłbym z tych ifów, żeby oddzielić logikę (co potwór robi, jak się zachowuje) ze szczegółami implementacji grafiki (jak to będzie wyglądać).

W szczególności tekstura wydaje się zależeć wprost od stanu, więc może napisać drugi zestaw ifów, które będą przeliczać stan na teksturę.
Albo zrobić metodę do zmiany stanu (zamiast zmieniać go wprost) i w tej metodzie mogła być ustalana tekstura.

0

Nawet jeśli to działa to jest to tragedia. Ifologia stosowana. Switch to tylko pudrowanie syfu. Zachęcam do zainteresowania się skończoną maszyną stanów i jak najlepiej zaimplementować stan i tranzycie między nimi (tablica/lista stanów i przejść) oraz towarzyszące akcje. Warto zastosować wzorzec stretegii. Warto zaimplementować coś takiego w celu treningowym ale docelowo doradzał bym użyć gotowej biblioteki.

0

w takim razie, jak ty byś napisał całą logikę potworów ? bo nie rozumiem...

3

@tBane jeśli chcesz wiedzieć czym jest maszyna stanów i jak wygląda typowa jej implementacja, to musisz co nieco poczytać. Na YouTube też jest mnóstwo informacji na ten temat, łącznie z animacjami ilustrującymi grafy przejść pomiędzy stanami, więc cóż… trzeba zacząć od przyswojenia teorii. 😉

Trochę to też bez sensu jest, bo samo znaczenie maszyny stanów jest ogólne i określa jedynie tyle, że operuje na skończonej liczbie stanów i w nieskończoność przełącza się pomiędzy nimi. A to czy taki automat zostanie zaimplementowany w formie ifów, switcha, dispatch table czy w pełni obiektowo, to już kwestia drugorzędna. Nawet twój obecny kod implementuje skończoną maszynę stanów, bo spełnia wszystkie jego założenia — obsługuje skończoną liczbę stanów i bez końca przełącza się pomiędzy nimi, w ściśle określony sposób.

To w jaki sposób zaimplementować taki automat, zależy od języka, paradygmatu oraz własnych wymagań/preferencji — nie ma jednej, właściwej implementacji. Ty piszesz w C++, więc możesz do tego celu wykorzystać klasy. Tutaj masz taki w miarę przystępny artykuł, tak na początek — Implementing a Finite State Machine in C++. Zapewne jeden artykuł nie wystarczy, bo maszyny stanów nie są najprostsze w implementacji, ale zawsze można poszukać innych, jako dopełnienie.

0

Działająca metoda bez implementacji maszyny stanu:

void update(float dt) {

	switch (state) {
	case states::fight:

		if (playerInAttackRange()) {
			if (cooldown <= 0.0f) {
				player->takeDamage(2);
				cooldown = attackTime;
				frame = 0;
			}

			frame = cooldown / attackTime * 4.0f - 1.0f;
			sprite->setTexture(*fightTextures[direction * 4 + frame]);
		}
		else if (playerInViewRange()) {
			state = states::run;
			target = player->position;
		}
		else
		{
			state = states::run;
		}
		break;

	case states::run:
		if (playerInAttackRange())
			state = states::fight;
		else if (playerInViewRange()) {
			target = player->position;
			state = states::run;
			float distance = 15.0f * stepSize * dt;
			goToTarget(distance);
			calculateCurrentFrame(dt);
			sprite->setTexture(*runTextures[direction * 4 + frame]);
		}
		else if (position == target) {
			state = states::idle;
		}
		else
		{
			float distance = 15.0f * stepSize * dt;
			goToTarget(distance);
			calculateCurrentFrame(dt);
			sprite->setTexture(*runTextures[direction * 4 + frame]);
		}
		break;

	case states::idle:
		if (playerInAttackRange()) {
			state = states::fight;
		}
		else if (playerInViewRange()) {
			target = player->position;
			state = states::run;
		}
		else {

			if ( rand()%600 == 0) {	// HERE HELP ME
				target.x = pointBase.x + rand() % 300 - 150;
				target.y = pointBase.y + rand() % 300 - 150;
				state = states::run;
			}
			
		}
		calculateCurrentFrame(dt);
		sprite->setTexture(*idleTextures[direction * 4 + frame]);
		break;
	}
	

	mouseOvering();

	if (cooldown >= 0.0f)
		cooldown -= dt;

	sprite->setPosition(position);
	collider->setPosition(position);
	viewRangeArea->setPosition(position);
	attackRangeArea->setPosition(position);

	textname->setPosition(position.x, position.y - height - 30);
	panelHP->setPosition(position.x, position.y - height - 10);
	panelHPcurrent->setPosition(position.x, position.y - height - 10);
	
}
2

Po prostu uruchom grę i graj w nią , i zobacz czy się fajnie gra.

0

tak właśnie robię :-) ale czasem nie mam pomysłu co dodać lub jak coś zmienić i wtedy wrzucam post. W tym przypadku nie jestem pewien czy akurat logika potworków jest dobrze napisana a nie jestem w tym ekspertem więc zbieram rady.

0

Potwór powinien się szwendać jak mu gracz zniknął

1
tBane napisał(a):

tak właśnie robię :-) ale czasem nie mam pomysłu co dodać lub jak coś zmienić i wtedy wrzucam post. W tym przypadku nie jestem pewien czy akurat logika potworków jest dobrze napisana a nie jestem w tym ekspertem więc zbieram rady.

Nie myśl o tym czy jest "dobrze napisana", tylko czy się dobrze gra w grę - przecież o to chodzi w grach, nie?

1

Jeśli nie masz pomysłu, a bardzo chcesz (ot w ramach ćwiczenia) rozbudować AI, to zawsze możesz kuknąć na inne gry i sprawdzić jak zachowują się różne postacie, w tym potworki. Póki robisz ten projekt głównie dla siebie, to możesz nawet i klona innej gry stworzyć.

2
tBane napisał(a):

-gdy potwór nic nie robi to co jakiś czas szuka sobie celu do którego może się przejść

Mam pytanie odnośnie założeń, może ja jestem odskulowy trochę, ale czy w grach RPG nie jest zazwyczaj tak, że potwory stoją mniej więcej w z góry wyznaczonych miejscach? Jak ma wyglądać design gry, gdzie potworki mogą sobie losowo chodzić po całej mapie? Co, jeżeli mamy pecha i na całej mapie jest pusto, a wszystkie stworki znalazły się w mniej więcej jednym miejscu, więc jak gracz tam pójdzie, to się wszystkie razem na niego rzucą i go zniszczą?

roark.dev napisał(a):

Nawet jeśli to działa to jest to tragedia. Ifologia stosowana. Switch to tylko pudrowanie syfu. Zachęcam do zainteresowania się skończoną maszyną stanów i jak najlepiej zaimplementować stan i tranzycie między nimi (tablica/lista stanów i przejść) oraz towarzyszące akcje. Warto zastosować wzorzec stretegii. Warto zaimplementować coś takiego w celu treningowym ale docelowo doradzał bym użyć gotowej biblioteki.

iks de

1
target.x = pointBase.x + (rand() % 3 - 1) * 100;
target.y = pointBase.y + (rand() % 3 - 1) * 100;

Potwory poruszają się w ograniczonym polu tzn. wokół punktu pointBase

0

Racja, zagapiłem się, przepraszam za zawracanie głowy.

0

nie ma problemu :-)

1

@tBane: teraz zauważyłem, że w tym snippecie masz linijkę z komentarzem:

if ( rand()%600 == 0) {	// HERE HELP ME

Potrzebujesz jakiejś pomocy? Chodzi o ten licznik, który zaproponowałem wcześniej?

1

Nie, pomocy już nie potrzebuję. Wymyśliłem coś takiego i działa :-)

if (cooldown <= 0) {
	cooldown = 3 + rand()%5;
	target.x = pointBase.x + rand() % 300 - 150;
	target.y = pointBase.y + rand() % 300 - 150;
	state = states::run;
}

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.