start / blog / engine
Veröffentlicht: 24. Mai 2026 9 Min. Lesezeit engine
Unreal Blueprint oder C++: die echten Kosten in Millisekunden

Unreal Blueprint oder C++: die echten Kosten in Millisekunden

Die Blueprint VM bleibt in den meisten Szenen unsichtbar, doch bei Zehntausenden Nodes pro Tick kostete sie 7 ms. Hier ist die hybride Aufteilung mit Zahlen.

Als 41.000 Nodes in einem einzigen Tick liefen

In einem Tower-Defence-Projekt Ende 2023 stieg die Frame-Zeit von 11,2 ms auf 19,4 ms, sobald Welle 14 begann. Im Editor sah alles noch vertretbar aus; in einem gepackten Development Build war der Unterschied offensichtlich. Die oberste Zeile in Unreal Insights hieß BlueprintTime und verschlang allein 7,1 ms.

Schuld war der Event Tick in BP_TowerBase. Jeder Turm lief mit einem ForEachLoop über alle 220 Gegner, berechnete eine quadrierte Distanz und wählte den nächsten aus. 24 Türme mal 220 Gegner, dazu eine Handvoll Operationen pro Node, ergaben rund 41.000 Node-Ausführungen pro Frame.

Das Problem war nicht, dass „Blueprint langsam ist“. Das Problem war eine O(n·m)-Suche, die in jedem einzelnen Frame in einer virtuellen Maschine lief. Diesen Code nach C++ zu verschieben senkt die Kosten tatsächlich, aber der eigentliche Fehler lag im Algorithmus, und genau diese beiden Dinge werden in der Debatte Unreal Blueprint oder C++ ständig vermischt.

Wir waren damals zu dritt im Team, und niemand von uns hat die Blueprint-Seite überhaupt profiliert. Als die Frame-Zeit wegrutschte, schauten wir zuerst auf die Draw Calls, dann auf die Schatteneinstellungen, dann auf die Texturauflösungen. Nach zwei verlorenen Tagen reichte es, Insights zu öffnen und die richtige Zeile zu lesen. Der Rest dieses Beitrags sind die Regeln, die ich aufgeschrieben habe, um diese zwei Tage nicht ein zweites Mal zu verlieren.

Wo Blueprint Performance wirklich messbar wird

Für eine belastbare Zahl habe ich einen schlichten Test gefahren: eine leere Funktion 100.000 mal aufrufen. Auf der C++-Seite lag die Gesamtzeit bei 0,4 ms, dieselbe Funktion im Blueprint brauchte 6,8 ms. Das sind rund 68 ns Overhead pro Aufruf.

68 ns sehen nach nichts aus, und meistens sind sie genau das. Für einen 200 Nodes langen Ablauf zum Öffnen einer Tür oder für die Logik hinter einem Inventarbildschirm bleiben die Kosten unter der Rauschgrenze; unterhalb von etwa 2.000 Nodes pro Frame lag der Gesamteffekt in keiner meiner Messungen über 0,15 ms.

Der Punkt, ab dem es kein Rauschen mehr ist, ist klar: Zehntausende Nodes pro Frame. Meine grobe Schwelle lautet: Läuft ein Blueprint in jedem Frame und enthält er eine Schleife, ist diese Logik ab sofort ein C++-Kandidat. Bei Abläufen, die von einmaligen Events ausgelöst werden, lohnt die Diskussion über den VM-Overhead nicht.

Dazu kommt eine Messfalle. In PIE wirken Blueprint-Aufrufe wegen der Debug-Instrumentierung teurer, als sie tatsächlich sind, deshalb führt eine Entscheidung auf Basis von Editor-Zahlen in die Irre. Ich verschiebe nichts nach C++, bevor ich einen gepackten Development Build mit stat game und Insights geprüft habe.

Hybrider Workflow: Kern in C++, Tuning im Blueprint

Zielauswahl, Schadensberechnung, Cooldown-Verwaltung und die State Machine sind als C++ nach AOFKTowerBase gewandert. Die Zielsuche läuft nicht mehr pro Frame, sondern auf einem Timer von 0,2 Sekunden über ein räumliches Grid. Im Blueprint geblieben sind: Effekt-Timing, Audio-Trigger, UI-Feedback und die Werte, an denen der Designer ständig dreht.

Die Frame-Zeit fiel von 19,4 ms auf 11,8 ms, BlueprintTime von 7,1 ms auf 0,9 ms. Das kam nicht alles von der VM: rund 5 ms gehen auf die Änderung des Algorithmus, 2,6 ms auf den Wechsel zu nativem Code. Ohne diese Trennung wäre der Satz „C++ hat es 40 % schneller gemacht“ schlicht unehrlich.

Zwei Alternativen haben wir verworfen. Alles in C++ war technisch am schnellsten, aber ein Designer, der einen einzigen Schadenswert testen wollte, saß 90 Sekunden Kompilierzeit plus Editor-Neustart ab; für jemanden mit 40 Versuchen am Tag ist das nicht tragbar. Alles im Blueprint zu lassen und nur den Tick abzuschalten brachte gar nichts, weil damit die eigentlich teure Schleife nicht verschwand.

Die Frage, mit der ich die Grenze ziehe, ist simpel: Braucht eine Änderung dieses Werts eine Kompilierung? Wenn ja, und wenn ein Designer ihn mehrmals pro Woche anfasst, gehört der Wert in ein Blueprint oder in ein Data Asset. Die Balancing-Tabelle der Türme liegt bei uns als UDataAsset vor: C++ liest sie, der Designer bearbeitet sie im Editor, und niemand wartet auf einen Build.

AOFKCharacter.h
#pragma once #include "CoreMinimal.h" #include "GameFramework/Character.h" #include "AOFKCharacter.generated.h" UCLASS() class OFKGAME_API AOFKCharacter : public ACharacter { GENERATED_BODY() public: // Designer-tunable value, read-only in Blueprint so the rule stays in C++. UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Combat", meta = (ClampMin = "0.05", UIMax = "2.0")) float DashCooldown = 0.35f; UFUNCTION(BlueprintCallable, Category = "Combat") bool CanDash() const; protected: // C++ decides when it fires; Blueprint decides how it looks and sounds. UFUNCTION(BlueprintImplementableEvent, Category = "Combat") void OnDashStarted(float Cooldown); private: float LastDashTime = -1000.f; };
cppAOFKCharacter.h

Mit UPROPERTY die richtige Oberfläche freigeben

Wie gut ein hybrider Aufbau funktioniert, hängt daran, was C++ dem Blueprint exakt freigibt. Tunbare Werte gehen als UPROPERTY(EditAnywhere, BlueprintReadOnly) heraus: Der Designer kann den Wert im Editor ändern, aber zur Laufzeit nicht hineinschreiben. Ein meta = (ClampMin, UIMax) entfernt die Hälfte der Bugs vom Typ „da hat jemand versehentlich 0 eingetragen“, bevor sie überhaupt entstehen.

Auf der Funktionsseite zählt der Unterschied zwischen drei Specifiern. BlueprintCallable ist das Blueprint, das C++ eine Frage stellt; BlueprintImplementableEvent ist C++, das dem Blueprint sagt „das ist gerade passiert, kümmere du dich um die Optik“; BlueprintNativeEvent liefert eine Standardimplementierung in C++, die das Blueprint bei Bedarf überschreiben darf. Die Regel ist kurz: C++ entscheidet, wann etwas passiert, das Blueprint entscheidet, wie es aussieht.

Der häufigste Fehler, den ich sehe, ist ein BlueprintReadWrite auf jedem Feld. Ist es einmal offen, schreiben Designer in Zustandsvariablen, und die Invarianten, die man in C++ schützt, brechen leise weg; bei uns zeigte sich das als Munitionszähler, der ins Negative rutschte. Die zweite Falle ist das Umbenennen: Wer den Namen einer UPROPERTY ändert, zerlegt still die Blueprint-Referenzen, deshalb benennen wir kein Feld ohne einen Eintrag in den Core Redirects um.

Es gibt auch eine Kompilierzeit-Seite. Wer eine UPROPERTY in einem Header anfasst, baut alles neu, was diesen Header einbindet; bei uns waren das auf einer ordentlichen Maschine bis zu 4 Minuten. Die häufig geänderten Einstellungen in ein kleines, separates Struct zu bündeln beschleunigte die Schleife des Designers und senkte sichtbar die Zahl der Full Rebuilds pro Tag.

Was sich nach dem Aus für die Nativization änderte

Um 4.26 herum haben sich einige Teams auf Blueprint Nativization gestützt, und wir haben es eine Weile ebenfalls probiert. Der gemessene Gewinn lag in schweren Szenen bei etwa 1,6 ms; im Gegenzug dauerte das Packaging 18 Minuten länger und wir hatten drei Bugs, die ausschließlich in nativisierten Builds auftraten. UE5 hat die Option komplett gestrichen.

Die praktische Folge: Es gibt keine späte Notausfahrt mehr, bei der der Compiler die Sache rettet. Den Hot Path in C++ zu haben ist kein Optimierungsschritt mehr, sondern eine Architekturentscheidung zu Projektbeginn. UE5 C++ als Pflaster zu behandeln, das man später aufklebt, ist genau das, was Teams mitten in der Produktion einholt.

Das hat auch eine gute Seite. Solange es die Nativization gab, schrieben Leute schwere Logik im Blueprint und planten, sie „später zu nativisieren“, und dieses Später kam nie. Ohne diese Option wurde es zur Pflicht, die Grenze von Anfang an zu ziehen, und das hat uns über die Zeit eine sauberere Codebasis hinterlassen.

An die Stelle der Nativization ist bei uns das Messen getreten. Vor jedem Release ziehen wir einen Insights-Trace aus einem gepackten Build, und sobald BlueprintTime über 1 ms geht, suchen wir den verantwortlichen Blueprint. Die Prüfung dauert fünfzehn Minuten. Im letzten Jahr hat sie dreimal eine ernste Regression erwischt, bevor sie ausgeliefert wurde.

Merge-Konflikte, sobald das Team wächst

Zu viert war es kein Problem, dass ein Blueprint ein binäres Asset ist. Bei elf Leuten fassten zwei- bis dreimal pro Woche zwei Personen dieselbe .uasset an, und eine .uasset lässt sich nicht mergen; wer verlor, machte seine Arbeit noch einmal. Die so verlorene Zeit in einem Sprint haben wir auf rund zwei Tage beziffert.

Wir haben in Perforce verpflichtendes Checkout und exklusive Locks eingerichtet. Die Konflikte hörten auf, dafür kam das Warten: Solange ein Blueprint gesperrt ist, wartet die zweite Person oder wechselt die Aufgabe. In C++ existiert dieses Problem nicht, denn Text merged zeilenweise und lässt sich im Pull Request tatsächlich lesen. Ein Blueprint zu reviewen bedeutet in der Praxis, Screenshots zu verschicken.

In der Unreal Engine Entwicklung wende ich heute vier Regeln an. Keine Logik, die in jedem Frame läuft, bleibt im Blueprint, und der Tick ist in jedem Blueprint, den wir anlegen, standardmäßig aus. Überschreitet ein Blueprint 150 Nodes, wandert ein Teil davon nach C++. Und wenn zwei Leute innerhalb eines Sprints dasselbe Blueprint bearbeiten müssen, gehört diese Logik ohnehin nach C++.

Diese Regeln haben genauso viel mit dem Tempo des Teams zu tun wie mit Performance. Nutzt Blueprint für die schnelle Iteration der Designer und C++ für das Rückgrat des Systems, und zieht diese Grenze vor der ersten Zeile Code. Später umzuziehen ist immer teurer; unsere Rechnung war ein Refactor von drei Wochen.

← Alle Beiträge