Du bist nicht angemeldet.

Werbeanzeige

Garzec

Alter Hase

  • »Garzec« ist der Autor dieses Themas

Beiträge: 700

Wohnort: Gießen

  • Private Nachricht senden

1

11.11.2019, 20:50

Grundstruktur für ein Slide Puzzle

Hallo zusammen,
ich würde gerne ein Slide Puzzle erstellen und habe mich vom Spielprinzip her von Slayaway Camp stark inspirieren lassen. Ich habe mal ein Gameplay Video rausgesucht



Ziel des Levels ist es, alle anderen Figuren auszuschalten und dabei selbst nicht von anderen NPCs ausgeschaltet zu werden. Das Spiel ist zwar ein Voxel Game, man kann es sich aber gut als 2D Grid vorstellen. Der Spieler gibt zu Beginn eines Zuges eine Bewegungsrichtung vor und danach ist der Input solange deaktiviert, bis alle Aktionen für diesen Zug abgehandelt sind.

Zunächst ist der logische Part von der Darstellung getrennt. Das heißt, wenn ein Haus eine bestimmte Anzahl von Zellen besetzt, sind diese Zellen (Grundriss) über ihre Außenseiten einfach nicht passierbar. Nun habe ich vom Code Design her mehrere Ansätze ausprobiert und immer wieder gemerkt, dass ich langfristig in Sackgassen rennen werde.

Eine Position (x|y) kann teilweise oder gar nicht passierbar sein. Das richtet sich danach, ob eine Außenseite ein Obstacle ist. Ein Beispiel wäre ein Zaun an einer der vier Außenseiten. Für diese kann ich mir die relative Position zum Zentrum merken, also (0|1) wäre die obere Außenseite.

Ebenso können auf einer Position NPCs stehen, die sich auf der Karte bewegen können. Wenn sich der Spieler auf einer Linie bewegt, agiert er immer mit dem Objekt vor sich, die beiden Objekte seitlich der Zielposition können auf den Spieler reagieren, der Spieler selbst tut aber nichts. Beispielsweise würde der Spieler den NPC vor sich töten, die beiden NPCs neben ihm würden aber wegrennen. Befindet sich beispielsweise ein Lichtschalter an der oberen Außenseite der Zelle hinter der Zielposition (Spieler kommt von unten nach oben) dann interagiert der Spieler damit natürlich nicht, er müsste sich direkt auf dieses Feld bewegen. Wichtig ist auch, ob sich zwischen den beiden Zellen ein Obstacle befindet, ein NPC hinter einer Wand interagiert ebenfalls nicht mit dem Spieler.

Auf den Zellen können sich nicht nur NPCs befinden. Es könnte sich dort beispielsweise auch ein Loch oder ein Feuer befinden, die andere Figuren töten, sobald diese drüberlaufen. Ich rechne damit, dass das Feuer nur zur Darstellung in der Mitte ist und sich an allen vier Außenseiten "Death-Trigger" befinden werden. Da diese die Bewegung der Figur stoppen werden, muss bei der Berechnung der Zielposition darauf geachtet werden. Andererseits gibt es auch Druckplatten, die zwar eine Aktion auslösen, die Bewegung der Figur aber nicht stoppen.

Nun zu meinem bisherigen Ansatz:
Den gesamten Zug kann man vorher berechnen und dann dementsprechend die Animationen laufen lassen. Prinzipiell ist ALLES erstmal ein Trigger, ob Feuer oder "Weglaufen", wenn der Spieler neben der Figur steht. Dementsprechend hat jede Position (x|y) an jeder Außenseite 0 - n Trigger. Da sich auch noch bewegliche Dinge auf der Zelle befinden können, gilt das Gleiche auch noch einmal für diese. Wenn sich eine Figur zu einer anderen Zelle bewegt, muss man ja irgendwie wissen, welche Trigger mit rübergehen und welche an der Position bleiben.

Bei den Obstacles kann man sich entscheiden, ob diese auch Trigger sind => Bewegung stoppen oder ob die Außenseiten einer Position oder eines Objektes ein Obstacle haben.

Bewegt sich der Spieler zu einem NPC, bleibt dieser vor ihm stehen und der Trigger des NPCs kümmert sich darum, dass der Spieler diesen tötet.

Irgendwie halte ich das aber für falsch, weil eigentlich sollte sich ja der Spieler selbst darum kümmern, dass er den NPC tötet. Ebenso gibt es das Problem, dass beispielsweise ein Feuer keine Obstacles hat. Man kann also nicht berechnen, dass der Spieler beim Feuer stehen bleibt. Man müsste explizit beim Trigger anfragen, ob dieser die Bewegung einer Figur stoppt.

Vielleicht hat ja jemand eine Idee, wie man die Logik so unter einen Hut bringt, dass man ein skalierbares System hat. Es muss nämlich nicht immer sein, dass der Spieler die NPCs sofort tötet, es ist auch möglich, dass manche NPCs eine undurchdringbare Rüstung haben und sich dann zum Spieler drehen und diesen töten...

Ich glaube die Information spielt hier keine Rolle, aber ich setze das Spiel mit Unity/C# um.

Vielen Dank schon einmal :)

Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von »Garzec« (11.11.2019, 21:09)


Sacaldur

Community-Fossil

Beiträge: 2 329

Wohnort: Berlin

Beruf: FIAE

  • Private Nachricht senden

2

13.11.2019, 11:00

Wenn du strikt bei dem Beispiel bleibst (was es so ja in vielen Variationen gibt), dann gibt es einerseits den Inhalt der Felder (Personen, Spieler, etc.), und es gibt die Bereiche zwischen den Feldern (Zäune), die bestimmen, ob man vom einen zum benachbarten Feld kommt. Wenn also der Spieler sich von einem Feld um ein Feld in eine Richtung bewegen will, schaut er zuerst auf den Bereich zwischen den Feldern. Ist dieser frei schaut er auf das Nachbarfeld. Ist dort eine Person, wird diese getötet, ist es blockiert bleibt er stehen, und wenn es frei ist, dann bewegt er sich ein Feld weiter und schaut für die gleiche Richtung, ob er sich ein weiteres Mal bewegen kann.
Feuer könnte dabei gehandhabt werden wie du willst, bspw. dass es die Bewegung nicht aufhält aber dafür Schaden macht, oder dass der Spieler und NPCs anhalten, solange es an ist, oder...
Spieleentwickler in Berlin? (Thema in diesem Forum)
---
Es ist ja keine Schande etwas falsch zu machen, als Programmierer tu ich das täglich, [...].

Garzec

Alter Hase

  • »Garzec« ist der Autor dieses Themas

Beiträge: 700

Wohnort: Gießen

  • Private Nachricht senden

3

13.11.2019, 12:15

Also jetzt in diesem Moment, hab ichs so, dass die Zelle als statisches Objekt ihre Obstacles und Trigger haben kann. Die Obstacles wären dann zB Zäune an Außenseiten oder vier Obstacles => Baum in der Mitte. Ein Trigger an einer Außenseite einer Zelle wäre zB ein Lichtschalter.

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Cell : MonoBehaviour
{
    [SerializeField]
    Vector2Int[] obstacleDirections;

    [SerializeField]
    CellSideTriggersInfo[] sideTriggers;

    public Vector2Int[] ObstacleDirections { get { return obstacleDirections; } }

    public CellSideTriggersInfo[] SideTriggers { get { return sideTriggers; } }
}

[Serializable]
public class CellSideTriggersInfo
{
    [SerializeField]
    Vector2Int relativePosition;

    [SerializeField]
    Trigger[] triggers;

    public Vector2Int RelativePosition { get { return relativePosition; } }

    public Trigger[] Triggers { get { return triggers; } }
}


Dann gibt es noch die Objekte auf der Zelle, also die NPCs zum Beispiel. Alles, was auf der Zelle steht, nenne ich mal SurfaceObject. Da merke ich mir aktuell folgende Trigger

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SurfaceObject : MonoBehaviour
{
    [SerializeField]
    private Trigger[] passiveFrontTriggers; // Ein Objekt bewegt sich auf dieses frontal zu

    [SerializeField]
    private Trigger[] passiveSideAndBackTriggers; // Ein Objekt bewegt sich auf dieses seitlich oder von hinten zu

    [SerializeField]
    private Trigger[] activeFrontTriggers; // Dieses Objekt bewegt sich frontal auf ein anderes zu

    [SerializeField]
    private Trigger[] activeSideAndBackTriggers; // Dieses Objekt bewegt sich seitlich oder von hinten auf ein anderes zu

    public Trigger[] PassiveFrontTriggers { get { return passiveFrontTriggers; } }

    public Trigger[] PassiveSideAndBackTriggers { get { return passiveSideAndBackTriggers; } }

    public Trigger[] ActiveFrontTriggers { get { return activeFrontTriggers; } }

    public Trigger[] ActiveSideAndBackTriggers { get { return activeSideAndBackTriggers; } }

    public Vector2Int GridPosition { get { return transform.GridPosition(); } } // GridPosition() wandelt die 3D Koordinaten in die Position auf dem Grid um
}


Global merke ich mir dann die Zellen per

C#-Quelltext

1
Cell[,] cells


und die SurfaceObjects per

C#-Quelltext

1
Dictionary<Vector2Int, SurfaceObject> surfaceObjects


Ein Trigger macht erstmal "irgendwas". Da bin ich noch nicht weit gekommen, ich hoffe aber, dass mir die Scriptable Objects weiterhelfen werden

C#-Quelltext

1
2
3
4
5
6
7
public abstract class Trigger : ScriptableObject
{
    public virtual void Execute()
    {
        // ...
    }
}


So könnte ich dann zumindest neue Trigger designen und diese anderen Objekten im Inspector zuweisen. Jedenfalls kann ich so schon einmal die Bewegung berechnen, indem ich gucke, ob ich noch in Bounds bin, ob meine aktuelle Zelle ein Obstacle in Bewegungsrichtung hat, ob die nächste Zelle ein Obstacle in entgegengesetzter Richtung hat und ob sich auf der nächsten Zelle ein SurfaceObject befindet.

Was da halt noch fehlt wäre die Prüfung, ob sich auf dem Weg ein Trigger befindet, der deine Bewegung stoppen würde. Also wenn man beispielsweise durchs Feuer läuft, würde man weiterlaufen, weil diese Zelle ja keine Obstacles hat. Die Bewegung muss dann aber beendet werden.

Und meine Trigger müssen eben in der Lage sein, ins Spiel einzugreifen. Also die Execute Methode benötigt noch ein paar Parameter von wegen "wer hat es ausgelöst", "wer ist betroffen", etc. damit dieser Trigger sagen kann "Auslöser tötet Betroffenen".

Ich glaube ganz so schwierig ist es gar nicht, ich muss nur mal auf die Masterlösung kommen :D

4

13.11.2019, 22:35

Mein Ansatz wäre ein 2D-Grid mit ungerader Anzahl in beide Richtungen. Auf Datenebene sind auch die Zwischenräume jeweils Felder. Jedes Feld besitzt ein Enum. Der erste Wert bedeutet freies Feld, jede andere Nummer beinhaltet ein Objekt. Es gibt 2 verschiedene Enums (für bessere Übersicht): für Zwischenräume wie Zäune und für die tatsächlich grafisch dargestellten Spielfelder. Es wird dabei aber auch ungenutzte Felder geben. Bewegt sich der Spieler nun, prüft das Spiel Feld für Feld die Enums. Je nach Enum führt das Spiel dann den entsprechenden Code (auch mehrere Aktionen möglich) aus wie:
- ein weiteres Feld prüfen,
- Spieler bis Feld X bewegen,
- umliegende Felder prüfen
- NPCs wegrennen lassen,
- ...

So umgehst du jedenfalls Probleme mit Obstacle wie bei deinem Feuer beschrieben und kannst leicht neue Funktionen einbauen. Soll sich ein NPC bewegen, geht dieser dann ebenfalls Feld für Feld durch. Das Enum für je Start- und (gegebenenfalls) Zielfeld muss dann automatisch geupdatet werden. Die grafische Bewegung ist dann, wie von dir gesagt, erst nach den Berechnungen.

Garzec

Alter Hase

  • »Garzec« ist der Autor dieses Themas

Beiträge: 700

Wohnort: Gießen

  • Private Nachricht senden

5

14.11.2019, 08:42

@Half Danke. Leider habe ich noch nicht ganz den Nutzen nachvollziehen können. Also bei meinem zuletzt geposteten Ansatz hätte den Trigger noch um sowas erweitert

C#-Quelltext

1
2
3
4
5
6
7
8
9
public abstract class Trigger : ScriptableObject
{
    public virtual void Execute(SurfaceObject triggeredBy, SurfaceObject attachedTo, Action onFinished)
    {
        Debug.Log("Triggered...");

        onFinished(); // Wir brauchen den Callback, damit wir innerhalb des Triggers auch Coroutinen starten können und auf diese warten können
    }
}


Und da die Trigger ja in einer Schleife abgearbeitet werden, hätte ich dann gesagt, jeder Trigger stoppt dich erstmal, nur es gibt noch einen "ContinueMovement" Trigger, der das Movement einer Figur am Ende dann noch einmal neu anstößt, falls benötigt.

So umgehst du jedenfalls Probleme mit Obstacle wie bei deinem Feuer beschrieben und kannst leicht neue Funktionen einbauen


Wie sähe das dann bei deinem Ansatz aus? Aktuell könnte ich über die abstrakte Trigger Klasse ja auch erstmal einbauen, was ich gerne haben möchte.

6

15.11.2019, 01:16

Naja dein Ansatz ist jedes Objekt zu "vertriggern" und den Code darüber ausführen zu lassen. Mein Ansatz ist über die Spielerbewegung selbst die Events auszulösen, indem in Bewegungsrichtung die Felder nach "Inhalt" (Enumwert) mittels Loop überprüft werden. Die Lösung zielte vorallem auf deine Aussage im Anfangspost ab:

Zitat

Bewegt sich der Spieler zu einem NPC, bleibt dieser vor ihm stehen und der Trigger des NPCs kümmert sich darum, dass der Spieler diesen tötet.

Irgendwie halte ich das aber für falsch,
weil eigentlich sollte sich ja der Spieler selbst darum kümmern, dass er den NPC tötet. Ebenso gibt es das Problem, dass beispielsweise ein Feuer keine Obstacles hat. Man kann also nicht berechnen, dass der Spieler beim Feuer stehen bleibt. Man müsste explizit beim Trigger anfragen, ob dieser die Bewegung einer Figur stoppt.

Vielleicht hat ja jemand eine Idee, wie man die Logik so unter einen Hut bringt, dass man ein skalierbares System hat. Es muss nämlich nicht immer sein, dass der Spieler die NPCs sofort tötet, es ist auch möglich, dass manche NPCs eine undurchdringbare Rüstung haben und sich dann zum Spieler drehen und diesen töten...

Die ersten beiden Probleme sind bei mir automatisch gelöst, weil der Input des Spielers die jeweils benötigten Events auslöst und keine externen (also am Objekt befindlichen) Trigger. Und skalierbar ist es auf quasi unbegrenzte Größe.

Via externe Trigger kann man das natürlich auch alles lösen, aber ich stelle mir das etwas unhandlicher vor (vielleicht liegt das auch nur an mir). Mir schoss gestern lediglich als erstes die Idee mit den Enums durch den Kopf. Ich kann gerne nochmal ausführlicher werden, falls gewünscht/benötigt, nur muss ich das im Wortlaut machen, da ich kein C# beherrsche.

Garzec

Alter Hase

  • »Garzec« ist der Autor dieses Themas

Beiträge: 700

Wohnort: Gießen

  • Private Nachricht senden

7

15.11.2019, 18:40

@Half wenn ich deinen Ansatz jetzt richtig verstanden habe, würdest du ein 2x2 Grid so aufteilen, dass du folgende Konstellation hast (leider etwas doof darzustellen):

Feld | Freiraum | Feld
Freiraum | Freiraum
Feld | Freiraum | Feld

Das echte Feld und der Freiraum haben jeweils ein Enum.

Das Enum kann folgende Dinge enthalten => Freies Feld, Aktion 1, Aktion 2, Aktion 3 (wobei eine Aktion dann machen kann, was sie will, also auch aus mehreren Dingen bestehen kann)

Bei der Bewegung des Spielers oder des NPCs werden dann die Enums Schritt für Schritt geprüft. Heißt ich fange mit der Startposition an, prüfe den ersten Zwischenraum und dieser kümmert sich darum, dass ich bis zum nächsten Feld komme oder eben bereits blockiert werde? Wenn er mich weiter lässt, kümmert er sich darum, dass die Berechnung hinter ihm wieder neu angestoßen wird?

Der Zwischenraum selbst würde dann keine umliegenden Felder prüfen, sondern nur die echten Felder nehme ich an?

Und ich prüfe somit auch immer nur die Enums in Bewegungsrichtung (vor mir)?

8

16.11.2019, 01:58

1) 2x2 Grid: richtig. Es fehlt nur die Spielfeldbegrenzung (entweder über Enums "solider Block" oder vor der Enumabfrage eine Prüfung, ob das Feld valide ist, sprich überhaupt existiert).

2) Ja!

3) Ich hätte die Enums mit Inhalt ...
0 -> freies Feld
1 -> Spielercharakter (wegen NPC-Navigierung)
2 -> Objektart 1 (z.B. solider Block)
3 -> Objektart 2 (z.B. NPC)
4 -> Objektart 3
... usw gefüllt und die Aktionen daraus abgeleitet. Rein technisch gesehen macht das keinen großen Unterschied, aber vom Denkansatz fällt es vermutlich leichter Logikschusselfehler zu vermeiden - zumindest würde es mir so gehen.

4)Ich würde einen Loop aufbauen, der jeweils Freiraum und Feld in einem Durchgang prüft, mit Abbruchfunktion. Stellt der Loop schon beim Freiraum einen soliden Block (z.B. Zaun) fest, kann der Loop vorzeitig abgebrochen werden. Oder du kannst nicht auf das nächste Feld, weil der Freiraum blockiert, du kannst aber etwas hinwerfen um den NPC zu erschrecken/töten - das überlasse ich deiner Phantasie. ;) Aber ja so in etwa stelle ich mir das vor.

5)Wie gesagt, je Zwischenraum und Feld wird in einem Loop zusammengefasst. Die Prüfung müsste entweder am Ende der Looprunde stattfinden (wenn du jedes Nachbarfeld auf dem Weg ebenfalls prüfen möchtest) oder ganz am Ende nach Abschluss der Loops (wenn du nur am Zielfeld die umliegenden Felder prüfen willst).

6)Grundsätzlich ja. Wie bei 5) schon angedeutet kannst du das aber beliebig nach deinen Bedürfnissen modifizieren.

Du musst übrigens auch nicht alle Loops mit dem ersten Spieltick nach Userinput berechnet haben, durch den modularen Loop-Aufbau kannst du das auch "on the fly" machen, was bei größeren Maps mit weiteren Strecken gegebenenfalls sinnvoll für Performance sein kann.

Garzec

Alter Hase

  • »Garzec« ist der Autor dieses Themas

Beiträge: 700

Wohnort: Gießen

  • Private Nachricht senden

9

16.11.2019, 18:12

@Half Danke. Ich finde die Idee mit den Zwischenräumen ganz cool. Das entzerrt aber mWn erstmal nur die Zellen. Also die NPCs müssen ja auch noch berücksichtigt werden. Ich grübel noch etwas bei speziellen Fällen.

An einer Seite einer Position befindet sich an einer Wand ein Schalter. Das Ganze wird dann beim Zwischenraum hängen. Sicherlich wirst du beim Enum dann nicht "StopAndUseTrigger" haben, sondern anstatt 1 Enum mit Aktionen ein Array mit Aktionen nutzen? Man kann ja sagen, da diese nacheinander (Loop) abgearbeitet werden, ist an der Stelle 0 das Obstacle und an Stelle 1 dann "UseTrigger" oder so.

Die NPCs brauchen auch noch ein ausgebautes System, wenn ich zB mit dem Spieler so weit laufe, bis auf dem nächsten Feld ein NPC ist, dann kann ich diesen zwar töten, muss aber noch den NPCs links und rechts von mir mitteilen, dass ich der Spieler bin und sie irgendwas tun sollen. Einfache NPCs rennen weg, andere NPCs könnten dich angreifen, wenn sie in deine Richtung gucken.

Ebenso gibt es den Fall, dass du manche NPCs nicht töten kannst (schwer gepanzert), sie dich aber schon. Du musst also prüfen, ob du diesen Gegner überhaupt töten kannst und je nachdem tötest du ihn, oder er dich (dreht sich dann um)..

Zuletzt gäbe es noch den Fall, dass manche NPCs ein Feld vor sich überwachen. Bleibt der Spieler also vor ihrer Nase stehen, würden diese ihn angreifen.

###

Noch komplexere Fälle habe ich nicht geplant.

10

17.11.2019, 18:15

Freut mich, wenn es dir hilft. Das hatte ich alles schon mit eingeplant. :)

(1) Enums können geändert werden. Das bedeutet, steht ein NPC auf einem Feld, ändert er das Enum des Felds auf "NPC". Geht der NPC 3 Felder weiter, wird das erste Feld wieder "Freies Feld" und das letzte Feld "NPC". Die Felder dazwischen können ja so bleiben wie sie sind, da du nichts komplexeres vor hast.

(2) Das Enum für den Wandtrigger kann z.B. "solider Triggerblock" oder ähnlich sein (es könnte ja auch ein Bodenschalter geben, über den gelaufen werden kann). Ich würde für jeden mit "Freies Feld" beendeten Loop eine Variable zum Bewegen des SC (Spielercharakter) setzen. Damit bleibt der Charakter automatisch stehen wenn irgend ein anderes Enum ausgewählt wurde. Bei nicht-soliden Inhalten musst du halt extra nochmal die Variable des SC ändern, bevor du eine andere Klasse lädst. Sprich bei "solider Triggerblock" müsstest du lediglich die Klasse des speziellen Schalters aufrufen.

(3) Die seitlichen Felder prüfst du auf eine ganz ähnliche Art wie die Felder in Spielerbewegungsrichtung und ist ein Enum "NPC", dann wird die Fluchtaktion ausgelöst.

(4) Ich würde für jede NPC-Art einen eigenen Enumwert zuweisen, sollten es nur 2-5 Arten sein. Wird es zuviel, würde ich alle unter "NPC" zusammenfassen und danach eine Klasse laden, welche sich darum kümmert.

(5) Du kannst das Feld vor dem NPC als Enum "Monitored" oder so eingeben. Dann musst du wie bei (2) beschrieben die Bewegungsvariable setzen (kannst ja drauf laufen) und dann je nachdem ob der SC dort stehen bleibt oder nicht die Aktion ausführen oder nicht. Du kannst das aber auch wie bei (3) über die seitliche Prüfung auslösen.

Selbiges kannst du auch mit allen anderen beweglichen Objekten machen wie dem Schrank im Video, der umfällt.

Werbeanzeige