Prototyping eines Handyspiels, wo man anfängt und wie man es macht. Teil 2

Für diejenigen, die den ersten Teil verpasst haben - Teil 1
Nächster Teil - Teil 3

Wenn jemand Interesse daran hat, etwas über den von der Veranstaltung verwendeten Aggregator zu lesen, dann sind Sie hier , aber dies ist nicht erforderlich.

Also fangen wir an, alles auf einem Haufen zu sammeln




Rakete:

Basisraketenklasse
using DG.Tweening; using GlobalEventAggregator; using UnityEngine; namespace PlayerRocket { public class Rocket : PlayerRocketBase { [SerializeField] private float pathorrectionTime = 10; private Vector3 movingUp = new Vector3(0, 1, 0); protected override void StartEventReact(ButtonStartPressed buttonStartPressed) { transform.SetParent(null); rocketState = RocketState.MOVE; transform.DORotate(Vector3.zero, pathorrectionTime); } protected override void Start() { base.Start(); EventAggregator.Invoke(new RegisterUser { playerHelper = this }); if (rocketState == RocketState.WAITFORSTART) return; RocketBehaviour(); } private void FixedUpdate() { RocketBehaviour(); } private void RocketBehaviour() { switch (rocketState) { case RocketState.WAITFORSTART: if (inputController.OnTouch && !inputController.OnDrag) rocketHolder.RotateHolder(inputController.worldMousePos); break; case RocketState.MOVE: rigidbody.AddRelativeForce(Vector3.up*(config.Speed*Time.deltaTime)); forceModel.AddModificator(); break; case RocketState.STOP: Debug.Log(" "); rigidbody.velocity = Vector3.zero; rigidbody.drag = 50; rocketState = RocketState.COMPLETESTOP; break; case RocketState.COMPLETESTOP: break; default: rocketState = RocketState.COMPLETESTOP; break; } } } } 


Was brauchen wir, damit eine Rakete abhebt? Im Spielraum brauchen wir einen bedingten Planeten, mit dem wir starten, einen Startknopf und eine Rakete. Was sollte eine Rakete können?

  1. Warten Sie auf den Start
  2. Fliegen
  3. Von Modifikatoren betroffen sein
  4. Um aufzuhören

Das heißt, wir haben ein unterschiedliches Verhalten / einen unterschiedlichen Zustand der Rakete. Abhängig vom aktuellen Zustand sollte die Rakete ein unterschiedliches Verhalten aufweisen. Bei der Programmierung sind wir ständig mit einer Situation konfrontiert, in der ein Objekt viele radikal unterschiedliche Verhaltensweisen haben kann.

Für komplexes Verhalten von Objekten ist es besser, Verhaltensmuster zu verwenden, beispielsweise ein Zustandsmuster. Für die einfachen verwenden unerfahrene Programmierer oft viel, wenn nicht anders. Ich empfehle die Verwendung von switch und enum. Erstens ist dies eine klarere Unterteilung der Logik in bestimmte Phasen. Dank dieser Informationen wissen wir genau, in welchem ​​Zustand wir uns gerade befinden und was passiert. Es gibt weniger Möglichkeiten, den Code in eine Nudel von Dutzenden von Ausnahmen zu verwandeln.

Wie es funktioniert:

Zuerst beginnen wir mit den Staaten, die wir brauchen:

  public enum RocketState { WAITFORSTART = 0, MOVE = 1, STOP = 2, COMPLETESTOP = 3, } 

In der Elternklasse haben wir ein Feld -
 protected RocketState rocketState; 

Standardmäßig wird ihm der erste Wert zugewiesen. Enum selbst legt die Standardwerte fest, aber für Daten, die von oben geändert oder von Spieledesignern konfiguriert werden können - wofür habe ich die Werte manuell festgelegt? Um dem inam irgendwo einen weiteren Wert hinzufügen zu können und die gespeicherten Daten nicht zu verletzen. Ich rate Ihnen auch, Flaggenaufzählung zu studieren.

Weiter:

Wir definieren das Verhalten selbst in einem Update, abhängig vom Wert des rocketState-Feldes

  private void FixedUpdate() { RocketBehaviour(); } private void RocketBehaviour() { switch (rocketState) { case RocketState.WAITFORSTART: if (inputController.OnTouch && !inputController.OnDrag) rocketHolder.RotateHolder(inputController.worldMousePos); break; case RocketState.MOVE: rigidbody.AddRelativeForce(Vector3.up*(config.Speed*Time.deltaTime)); forceModel.AddModificator(); break; case RocketState.STOP: Debug.Log(" "); rigidbody.velocity = Vector3.zero; rigidbody.drag = 50; rocketState = RocketState.COMPLETESTOP; break; case RocketState.COMPLETESTOP: break; default: rocketState = RocketState.COMPLETESTOP; break; } } 

Ich werde entziffern, was passiert:

  1. Wenn wir warten, drehen wir einfach die Rakete in Richtung des Mauszeigers und stellen so die anfängliche Flugbahn ein
  2. Der zweite Zustand - wir fliegen, beschleunigen die Rakete in die richtige Richtung und aktualisieren das Modifikatormodell für das Auftreten von Objekten, die die Flugbahn beeinflussen
  3. Der dritte Zustand ist, wenn das Team bei uns ankommt, um anzuhalten. Hier arbeiten wir alles so aus, dass die Rakete anhält und sich in den Zustand übersetzt - wir haben vollständig angehalten.
  4. Der letzte Zustand ist, dass wir nichts tun.

Die Bequemlichkeit des aktuellen Musters - alles ist sehr leicht erweiterbar und einstellbar, aber es gibt nur eine schwache Verbindung - dann können wir einen Zustand haben, der eine Reihe anderer Zustände kombiniert. Hier entweder ein Flag inam, mit einer Komplikation der Verarbeitung, oder bereits zu "schwereren" Mustern wechseln.

Wir haben die Rakete herausgefunden. Der nächste Schritt ist ein einfaches, aber lustiges Objekt - der Startknopf.

Starttaste


Die folgenden Funktionen sind für sie erforderlich - geklickt, sie benachrichtigt, dass sie auf sie geklickt haben.

Start Button Class
 using UnityEngine; using UnityEngine.EventSystems; public class StartButton : MonoBehaviour, IPointerDownHandler { private bool isTriggered; private void ButtonStartPressed() { if (isTriggered) return; isTriggered = true; GlobalEventAggregator.EventAggregator.Invoke(new ButtonStartPressed()); Debug.Log(""); } public void OnPointerDown(PointerEventData eventData) { ButtonStartPressed(); } } public struct ButtonStartPressed { } 


Laut Spieldesign ist dies ein 3D-Objekt auf der Bühne, der Knopf soll in das Design des Startplaneten integriert werden. Okay, es gibt eine Nuance - wie kann man einen Klick auf ein Objekt in einer Szene verfolgen?

Wenn wir googeln, finden wir eine Reihe von OnMouse-Methoden, unter denen es einen Klick gibt. Es scheint eine einfache Wahl zu sein, aber es ist einfach sehr schlecht, angefangen mit der Tatsache, dass es oft schief funktioniert (es gibt viele Nuancen für das Verfolgen von Klicks), „Liebes“, bis hin zur Tatsache, dass es nicht so viele Brötchen gibt, die in UnityEngine.EventSystems enthalten sind.

Am Ende empfehle ich die Verwendung von UnityEngine.EventSystems und den Schnittstellen IPointerDownHandler, IPointerClickHandler. In ihren Methoden erkennen wir die Reaktion auf das Pressen, aber es gibt mehrere Nuancen.

  1. In der Szene muss ein EventSystem vorhanden sein. Dies ist ein Objekt / eine Klasse / eine Komponente der Einheit, die normalerweise beim Erstellen der Zeichenfläche für die Schnittstelle erstellt wird. Sie können sie jedoch auch selbst erstellen.
  2. Physik RayCaster muss auf der Kamera vorhanden sein (dies ist für 3D, für 2D-Grafiken gibt es einen separaten Racaster)
  3. In der Einrichtung muss sich ein Collider befinden

Im Projekt sieht es so aus:



Jetzt verfolgt das Objekt den Klick und diese Methode heißt:

 public void OnPointerDown(PointerEventData eventData) { ButtonStartPressed(); } private void ButtonStartPressed() { if (isTriggered) return; isTriggered = true; GlobalEventAggregator.EventAggregator.Invoke(new ButtonStartPressed()); Debug.Log(""); } 

Was ist hier los:

Wir haben ein Boolesches Feld, in dem wir verfolgen, ob die Taste gedrückt wurde oder nicht (dies ist ein Schutz gegen wiederholtes Drücken, damit nicht jedes Mal ein Startskript ausgeführt wird).

Als nächstes rufen wir das Ereignis auf - der Knopf wird gedrückt, den die Raketenklasse abonniert hat, und die Rakete in einen Bewegungszustand versetzt.

Ein bisschen vorausspringen - warum ist es hier und da für Events? Dies ist eine ereignisorientierte Programmierung. Erstens ist ein Ereignismodell billiger als eine kontinuierliche Datenverarbeitung, um ihre Änderungen herauszufinden. Zweitens ist dies die schwächste Verbindung. Wir müssen auf der Rakete nicht wissen, dass es einen Knopf gibt, dass jemand darauf gedrückt hat, und so weiter. Wir wissen nur, dass es ein Ereignis gibt, das gestartet werden muss. Wir haben es erhalten und handeln. Außerdem ist dieses Ereignis nicht nur für die Rakete interessant, zum Beispiel ist ein Feld mit Modifikatoren für dasselbe Ereignis signiert, es ist zu Beginn der Rakete versteckt. Dieses Ereignis kann auch für den Eingabecontroller von Interesse sein - und Benutzereingaben dürfen nach dem Start der Rakete nicht anders verarbeitet werden.

Warum mögen nicht viele Programmierer das Ereignisparadigma? Weil eine Menge Ereignisse und Abonnements für diese Ereignisse den Code leicht in Nudeln verwandeln, in denen überhaupt nicht klar ist, wo er beginnen soll und ob er irgendwo enden wird, ganz zu schweigen von der Tatsache, dass Sie auch Ihr Abbestellen / Abonnement überwachen und alle Objekte am Leben erhalten müssen.

Und deshalb verwende ich für die Implementierung von Ereignissen meinen Ereignisaggregator, der tatsächlich keine Ereignisse überträgt, sondern Datencontainer über Ereignisse und Klassen, die die Daten abonnieren, die sie interessieren. Außerdem überwacht der Aggregator selbst lebende Objekte und wirft tote Objekte aus Abonnenten. Dank der Übergabe des Behälters ist auch die Injektion möglich, Sie können uns einen Link zur interessierenden Klasse übergeben. Mithilfe des Containers können Sie leicht verfolgen, wer diese Daten verarbeitet und sendet. Für das Prototyping ist das eine tolle Sache.

Raketendrehung zur Bestimmung des Startpfades



Entsprechend dem Spieldesign sollte sich die Rakete um den Planeten drehen können, um die anfängliche Flugbahn zu bestimmen, jedoch nicht mehr als einen bestimmten Winkel. Die Drehung erfolgt durch Berühren - die Rakete folgt einfach dem Finger und ist immer auf die Stelle gerichtet, an der wir auf den Bildschirm gestoßen haben. Übrigens ist es nur der Prototyp, der es möglich gemacht hat, festzustellen, dass dies eine Schwachstelle ist, und es gibt viele mit dem Management verbundene Episoden, die an diese Funktionalität grenzen.

Aber in der Reihenfolge:

  1. Wir brauchen die Rakete, um uns relativ zum Planeten in Richtung der Schubkarre zu drehen
  2. Wir müssen den Drehwinkel festklemmen

Was die Rotation relativ zum Planeten betrifft - Sie können sich schlau um die Achse drehen und die Rotationsachse berechnen, oder Sie können einfach ein Objekt mit einem im Planeten zentrierten Dummy erstellen, die Rakete dorthin bewegen und den Dummy leise um die Z-Achse drehen. Der Dummy hat eine Klasse, die das Verhalten des Objekts bestimmt. Die Rakete dreht sich mit. Das Objekt, das ich RocketHolder genannt habe. Wir haben es herausgefunden.

Nun zu den Einschränkungen beim Drehen und Drehen in Richtung der Schubkarre:

Klasse RocketHolder
 using UnityEngine; public class RocketHolder : MonoBehaviour { [SerializeField] private float clampAngle = 45; private void Awake() { GlobalEventAggregator.EventAggregator.AddListener(this, (InjectEvent<RocketHolder> obj) => obj.inject(this)); } private float ClampAngle(float angle, float from, float to) { if (angle < 0f) angle = 360 + angle; if (angle > 180f) return Mathf.Max(angle, 360 + from); return Mathf.Min(angle, to); } private Vector3 ClampRotationVectorZ (Vector3 rotation ) { return new Vector3(rotation.x, rotation.y, ClampAngle(rotation.z, -clampAngle, clampAngle)); } public void RotateHolder(Vector3 targetPosition) { var diff = targetPosition - transform.position; diff.Normalize(); float rot_z = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg; transform.rotation = Quaternion.Euler(0f, 0f, rot_z - 90); transform.eulerAngles = ClampRotationVectorZ(transform.rotation.eulerAngles); } } 


Trotz der Tatsache, dass das Spiel theoretisch 3D ist, ist die gesamte Logik und das Gameplay tatsächlich 2D. Und wir müssen nur die Rakete um die Z-Achse in Richtung des Druckortes festziehen. Am Ende der Methode klemmen wir den Rotationsgrad um den im Inspektor angegebenen Wert. In der Awake-Methode können Sie die korrekteste Implementierung einer Klasseninjektion über einen Aggregator anzeigen.

Eingangssteuerung


Eine der wichtigsten Klassen ist es, der das Benutzerverhalten sammelt und verarbeitet. Drücken von Hotkeys, Gamepad-Tasten, Tastaturen usw. Ich habe eine ziemlich einfache Eingabe in den Prototyp, in der Tat müssen Sie nur 3 Dinge wissen:

  1. Gibt es einen Klick und seine Koordinaten
  2. Gibt es einen vertikalen Wisch und wie viel Wischen
  3. Arbeite ich mit Schnittstelle / Modifikatoren?

Klasse InputController
 using System; using UnityEngine; using UnityEngine.EventSystems; public class InputController : MonoBehaviour { public const float DirectionRange = 10; private Vector3 clickedPosition; [Header("     ")] [SerializeField] private float afterThisDistanceWeGonnaDoSwipe = 0.5f; [Header("  ")] [SerializeField] private float speedOfVerticalScroll = 2; public ReactiveValue<float> ReactiveVerticalScroll { get; private set; } public Vector3 worldMousePos => Camera.main.ScreenToWorldPoint(Input.mousePosition); public bool OnTouch { get; private set; } public bool OnDrag { get; private set; } // Start is called before the first frame update private void Awake() { ReactiveVerticalScroll = new ReactiveValue<float>(); GlobalEventAggregator.EventAggregator.AddListener(this, (ImOnDragEvent obj) => OnDrag = obj.IsDragging); GlobalEventAggregator.EventAggregator.AddListener<InjectEvent<InputController>>(this, InjectReact); } private void InjectReact(InjectEvent<InputController> obj) { obj.inject(this); } private void OnEnable() { GlobalEventAggregator.EventAggregator.Invoke(this); } void Start() { GlobalEventAggregator.EventAggregator.Invoke(this); } private void MouseInput() { if (EventSystem.current.IsPointerOverGameObject() && EventSystem.current.gameObject.layer == 5) return; if (Input.GetKeyDown(KeyCode.Mouse0)) clickedPosition = Input.mousePosition; if (Input.GetKey(KeyCode.Mouse0)) { if (OnDrag) return; VerticalMove(); OnTouch = true; return; } OnTouch = false; ReactiveVerticalScroll.CurrentValue = 0; } private void VerticalMove() { if ( Math.Abs(Input.mousePosition.y-clickedPosition.y) < afterThisDistanceWeGonnaDoSwipe) return; var distance = clickedPosition.y + Input.mousePosition.y * speedOfVerticalScroll; if (Input.mousePosition.y > clickedPosition.y) ReactiveVerticalScroll.CurrentValue = distance; else if (Input.mousePosition.y < clickedPosition.y) ReactiveVerticalScroll.CurrentValue = -distance; else ReactiveVerticalScroll.CurrentValue = 0; } // Update is called once per frame void Update() { MouseInput(); } } } 


Es ist alles in der Stirn und ohne Probleme, interessant kann die primitive Implementierung von reaktivem proprietär sein - als ich gerade mit dem Programmieren anfing, war es immer interessant herauszufinden, dass sich die Daten geändert haben, ohne die Daten ständig zu belüften. Nun, das ist es.

Es sieht so aus:

Klasse ReactiveValue
 public class ReactiveValue<T> where T: struct { private T currentState; public Action<T> OnChange; public T CurrentValue { get => currentState; set { if (value.Equals(currentState)) return; else { currentState = value; OnChange?.Invoke(currentState); } } } } 


Wir abonnieren OnChange und zucken, wenn sich nur der Wert geändert hat.

In Bezug auf Prototyping und Architektur - die Tipps sind dieselben, nur öffentlich zugängliche Eigenschaften und Methoden. Alle Daten sollten nur lokal geändert werden. Verarbeitung und Berechnung - nach getrennten Methoden addieren. Infolgedessen können Sie die Implementierung / Berechnungen jederzeit ändern, was sich nicht auf die externen Benutzer der Klasse auswirkt. Das ist alles für den Moment, im dritten letzten Teil - über Modifikatoren und Schnittstelle (Drag-Drop). Und ich habe vor, das Projekt auf Git zu stellen, damit ich sehen / fühlen kann. Wenn Sie Fragen zum Prototyping haben - fragen Sie, ich werde versuchen, diese klar zu beantworten.

Source: https://habr.com/ru/post/de453236/


All Articles