So kam es, dass ich oft Prototypen mache (sowohl für die Arbeit als auch für persönliche Projekte)
und ich möchte meine Erfahrungen teilen. Ich möchte einen Artikel schreiben, der interessant wäre, mich selbst zu lesen, und zunächst frage ich mich, woran sich eine Person orientiert, wenn sie diese oder jene Entscheidung bei der Umsetzung des Projekts trifft, wie sie das Projekt startet, es ist oft am schwierigsten zu beginnen.
Ein neues Beispiel, das ich mit Ihnen betrachten möchte, ist das Konzept eines zufälligen, auf Physik basierenden Puzzles.
Manchmal werde ich sehr offensichtliche Dinge erwähnen, um das Thema für Anfänger zu erweitern.Die Idee sieht so aus
Teil 2Teil 3Wir ordnen verschiedene Modifikatoren auf dem Spielfeld an, die die Richtung der Rakete ändern, anziehen, beschleunigen, abstoßen und so weiter. Die Aufgabe besteht darin, den Weg zwischen den Sternen zum nächsten Planeten zu ebnen, den wir brauchen. Das Gewinnen wird beim Landen / Berühren der Spur des Planeten berücksichtigt. Das Spielfeld ist vertikal, mehrere Bildschirme hoch, es wird davon ausgegangen, dass die Flugbahn den Boden des Bildschirms links / rechts einnehmen kann. Verlieren - Wenn Sie den Zielplaneten verpasst haben, mit einem anderen Planeten kollidiert sind, sind Sie weit über die Zone hinaus geflogen.
Und ich hoffe, wir alle verstehen, dass wir vor dem Beginn der Entwicklung und Umsetzung unserer Pläne zunächst einen Prototyp erstellen müssen - ein sehr grobes und schnelles Produkt, mit dem Sie schnell die grundlegenden Mechanismen ausprobieren und sich ein Bild vom Gameplay machen können. Wofür sind Prototypen gemacht? Der Spielprozess in unseren Köpfen und in der Praxis sind sehr unterschiedliche Dinge, was uns cool erscheint, für andere wird es völliger Horror sein, und oft gibt es kontroverse Momente im Projekt - Management, Spielregeln, Spieldynamik usw. usw. Es wird extrem dumm sein, die Prototypenphase zu überspringen, die Architektur durchzuarbeiten, die Grafiken zu liefern, die Level anzupassen und schließlich herauszufinden, dass das Spiel Scheiße ist. Aus dem Leben - in einem Spiel gab es Minispiele, ungefähr 10 Teile, nach dem Prototyping stellte sich heraus, dass sie furchtbar langweilig waren, die Hälfte wurde weggeworfen, die andere Hälfte wurde erneuert.
Tipp - Isolieren Sie zusätzlich zur grundlegenden Mechanik kontroverse Stellen und schreiben Sie die spezifischen Erwartungen an den Prototyp auf. Dies geschieht, um schwierige Momente nachzuladen. Eine der Aufgaben dieses Prototyps war beispielsweise, wie bequem und verständlich das Spielfeld aus mehreren Bildschirmen besteht und diese gewickelt werden müssen. Plan A - svayp musste umgesetzt werden. Plan B - die Möglichkeit, das Spielfeld zu vergrößern (falls erforderlich). Es gab auch mehrere Optionen für Modifikatoren. Die erste Idee ist im Screenshot zu sehen - wir legen den Modifikator und die Richtung seines Einflusses offen. Infolgedessen wurden die Modifikatoren einfach durch Kugeln ersetzt, die die Richtung der Rakete ändern, wenn sie die Kugel berühren. Wir beschlossen, dass dies eher beiläufig sein würde, ohne Flugbahnen usw.
Die allgemeine Funktionalität, die wir implementieren:- Sie können die anfängliche Flugbahn der Rakete einstellen, wobei der Grad der Abweichung von der Senkrechten begrenzt ist (die Rakete kann nicht mehr als einen Grad zur Seite gedreht werden).
- Es sollte einen Startknopf geben, mit dem wir die Rakete auf die Straße schicken
- Scrollen des Bildschirms beim Platzieren des Modifikators (zum Starten)
- Kamerabewegung hinter dem Player (nach dem Start)
- Schnittstellenfenster, mit dem Drag & Drop-Modifikatoren auf dem Feld implementiert werden
- Der Prototyp muss zwei Modifikatoren haben - Abstoßung und Beschleunigung
- Wenn du berührst, muss es Planeten geben, die du stirbst
- Wenn Sie berühren, muss es einen Planeten geben, den Sie gewinnen
Architektur
Ein sehr wichtiger Punkt, in der großen Entwicklung wird allgemein angenommen, dass der Prototyp so schrecklich wie schneller geschrieben wird, dann wird das Projekt einfach von Grund auf neu geschrieben. In der harten Realität vieler Projekte wachsen die Beine aus dem Prototyp heraus, was für die große Entwicklung schlecht ist - Kurvenarchitektur, Legacy-Code, technische Schulden, zusätzliche Zeit für das Refactoring. Nun, und die Indie-Entwicklung als Ganzes fließt reibungslos vom Prototyp zur Beta. Warum bin ich? Es ist notwendig, die Architektur sogar in einen Prototyp zu legen, auch wenn sie primitiv ist, damit Sie später nicht weinen oder vor Kollegen rot werden.
Vor jedem Start wiederholen wir immer - SOLID, KISS, DRY, YAGNI. Selbst erfahrene Programmierer vergessen Kiss und Yagni.
An welcher grundlegenden Architektur halte ich mich fest?Es gibt ein leeres Gameobject GameController in der Szene mit dem entsprechenden Tag, Komponenten / Mono-Bikes hängen daran. Es ist besser, es zu einem Fertighaus zu machen und dann einfach Komponenten nach Bedarf zum Fertighaus hinzuzufügen:
- GameController - (verantwortlich für den Status des Spiels, direkt zur Logik (gewonnen, verloren, wie viel Leben usw.)
- InputController - alles, was mit der Verwaltung von Spielern, dem Verfolgen von Tachi, Klicks, dem Klicken, dem Kontrollstatus usw. zu tun hat.
- TransformManager - In Spielen müssen Sie häufig wissen, wer wo ist, verschiedene Daten, die sich auf die Position des Spielers / der Feinde beziehen. Wenn wir zum Beispiel an einem Planeten vorbeifliegen, ist der Spieler besiegt, der Gamecontroller ist dafür verantwortlich, aber er muss die Position des Spielers kennen, von wo aus. Der Transformationsmanager ist genau die Essenz, die über Dinge Bescheid weiß
- AudioController - hier ist klar, es geht um Sounds
- InterfacesController - und hier ist klar, dass es um die Benutzeroberfläche geht
Das Gesamtbild ergibt sich: Für jede verständliche Aufgabe haben Sie einen eigenen Controller / eine eigene Entität, die diese Probleme löst. Dies hilft, Godlayk-Objekte zu vermeiden. Sie erhalten eine Vorstellung davon, wo Sie aus den Controllern, die wir Daten senden, graben können. Wir können jederzeit die Implementierung des Empfangs von Daten ändern. Öffentliche Felder sind nicht erlaubt, wir geben Daten nur über öffentliche Eigenschaften / Methoden. Wir berechnen / ändern Daten lokal.
Manchmal kommt es vor, dass der GameController aufgrund verschiedener spezifischer Logik und Berechnungen aufgeblasen ist. Wenn wir Daten verarbeiten müssen, ist es besser, eine separate Klasse GameControllerModel zu erstellen und dort auszuführen.
Und so begann der Code
Basisklasse für Raketenusing GlobalEventAggregator; using UnityEngine; using UnityEngine.Assertions; namespace PlayerRocket { public enum RocketState { WAITFORSTART = 0, MOVE = 1, STOP = 2, COMPLETESTOP = 3, } [RequireComponent(typeof(Rigidbody))] public abstract class PlayerRocketBase : MonoBehaviour, IUseForces, IPlayerHelper { [SerializeField] protected RocketConfig config; protected Rigidbody rigidbody; protected InputController inputController; protected RocketHolder rocketHolder; protected RocketState rocketState; public Transform Transform => transform; public Rigidbody RigidbodyForForce => rigidbody; RocketState IPlayerHelper.RocketState => rocketState; protected ForceModel<IUseForces> forceModel; protected virtual void Awake() { Injections(); EventAggregator.AddListener<ButtonStartPressed>(this, StartEventReact); EventAggregator.AddListener<EndGameEvent>(this, EndGameReact); EventAggregator.AddListener<CollideWithPlanetEvent>(this, DestroyRocket); rigidbody = GetComponent<Rigidbody>(); Assert.IsNotNull(rigidbody, " " + gameObject.name); forceModel = new ForceModel<IUseForces>(this); } protected virtual void Start() { Injections(); } private void DestroyRocket(CollideWithPlanetEvent obj) { Destroy(gameObject); } private void EndGameReact(EndGameEvent obj) { Debug.Log(" "); rocketState = RocketState.STOP; } private void Injections() { EventAggregator.Invoke(new InjectEvent<InputController> { inject = (InputController obj) => inputController = obj}); EventAggregator.Invoke(new InjectEvent<RocketHolder> { inject = (RocketHolder holder) => rocketHolder = holder }); } protected abstract void StartEventReact(ButtonStartPressed buttonStartPressed); } public interface IPlayerHelper { Transform Transform { get; } RocketState RocketState { get; } } }
Gehen wir die Klasse durch:
[RequireComponent(typeof(Rigidbody))] public abstract class PlayerRocketBase : MonoBehaviour, IUseForces, IPlayerHelper
Erstens, warum ist die Klasse abstrakt? Wir wissen nicht, welche Art von Raketen wir haben werden, wie sie sich bewegen werden, wie sie animiert werden, welche Spielfunktionen verfügbar sein werden (zum Beispiel die Möglichkeit, dass eine Rakete zur Seite springt). Daher machen wir die Basisklasse abstrakt, legen dort Standarddaten ab und legen die abstrakten Methoden fest, deren Implementierung für bestimmte Raketen variieren kann.
Es ist auch ersichtlich, dass die Klasse bereits eine Implementierung von Schnittstellen und ein Attribut hat, das im Gameplay die gewünschte Komponente hängt, ohne die die Rakete keine Rakete ist.
[SerializeField] protected RocketConfig config;
Dieses Attribut gibt an, dass der Inspektor über ein serialisierbares Feld verfügt, in das das Objekt eingepfercht ist. In den meisten Lektionen, einschließlich Unity, werden diese Felder veröffentlicht. Wenn Sie ein Indie-Entwickler sind, tun Sie dies nicht. Verwenden Sie private Felder und dieses Attribut. Hier möchte ich ein wenig darüber nachdenken, was diese Klasse ist und was sie tut.
Rocketconfig using UnityEngine; namespace PlayerRocket { [CreateAssetMenu(fileName = "RocketConfig", menuName = "Configs/RocketConfigs", order = 1)] public class RocketConfig : ScriptableObject { [SerializeField] private float speed; [SerializeField] private float fuel; public float Speed => speed; public float Fuel => fuel; } }
Dies ist ein ScriptableObject, in dem Raketeneinstellungen gespeichert werden. Dies erfordert den Datenpool, den Spieleentwickler benötigen - über die Klasse hinaus. Somit müssen Spieleentwickler nicht herumspielen und ein bestimmtes Spielprojekt mit einer bestimmten Rakete einrichten. Sie können nur diese Konfiguration reparieren, die in einem separaten Asset / einer separaten Datei gespeichert ist. Sie können die Laufzeitkonfiguration konfigurieren und sie wird gespeichert. Es ist auch möglich, verschiedene Skins für die Rakete zu kaufen, und die Parameter sind dieselben - die Konfiguration wandert nur dorthin, wo Sie sie benötigen. Dieser Ansatz wird gut erweitert - Sie können beliebige Daten hinzufügen, benutzerdefinierte Editoren schreiben usw.
protected ForceModel<IUseForces> forceModel;
Ich möchte auch darauf eingehen, dies ist eine generische Klasse zum Anwenden von Modifikatoren auf ein Objekt.
Forcemodel using System.Collections.Generic; using System.Linq; using UnityEngine; public enum TypeOfForce { Push = 0, AddSpeed = 1, } public class ForceModel<T> where T : IUseForces { readonly private T forceUser; private List<SpaceForces> forces = new List<SpaceForces>(); protected bool IsHaveAdditionalForces; public ForceModel(T user) { GlobalEventAggregator.EventAggregator.AddListener<SpaceForces>(this, ChangeModificatorsList); forceUser = user; } private void ChangeModificatorsList(SpaceForces obj) { if (obj.IsAdded) forces.Add(obj); else forces.Remove(forces.FirstOrDefault(x => x.CenterOfObject == obj.CenterOfObject)); if (forces.Count > 0) IsHaveAdditionalForces = true; else IsHaveAdditionalForces = false; } public void AddModificator() { if (!IsHaveAdditionalForces) return; foreach (var f in forces) { switch (f.TypeOfForce) { case TypeOfForce.Push: AddDirectionForce(f); break; case TypeOfForce.AddSpeed: forceUser.RigidbodyForForce.AddRelativeForce(Vector3.up*f.Force); break; } } } private void AddDirectionForce(SpaceForces spaceForces) {
Dies ist, was ich oben geschrieben habe - wenn Sie eine Art Berechnung / komplexe Logik ausführen müssen, legen Sie sie in eine separate Klasse. Es gibt eine sehr einfache Logik - es gibt eine Liste von Kräften, die auf eine Rakete wirken. Wir durchlaufen die Liste, schauen uns an, um welche Art von Leistung es sich handelt, und wenden eine bestimmte Methode an. Die Liste wird durch Ereignisse aktualisiert. Ereignisse treten beim Ein- / Beenden in das Modifikatorfeld auf. Das System ist sehr flexibel, erstens arbeitet es mit einer Schnittstelle (Hi-Kapselung), Benutzer von Modifikatoren können nicht nur Raketen / Spieler sein. Zweitens ein Generikum - Sie können IUseForces mit verschiedenen Nachkommen für Anforderungen / Experimente erweitern und diese Klasse / dieses Modell weiterhin verwenden.
Genug für den ersten Teil. Im zweiten Teil werden wir ein System von Ereignissen, Abhängigkeitsinjektionen, einen Eingabecontroller und eine Raketenklasse selbst betrachten und versuchen, sie zu starten.