C++ Outruns Unity DOTS Tenfold as Custom Engine Triples Labor Cost

Unity DOTS vs Handbuilt: Sample Project — Nick Caston

Nick Caston

A custom C++ engine delivers up to tenfold entity capacity over Unity DOTS while using nearly 90% less memory, but building basic graphics pipelines from scratch demands more than triple the development effort.

AI-assisted summary of Unity DOTS vs Handbuilt: Sample Project — original source. Check the original for full context.

Source published:

Key ideas

  • Nick Caston benchmarked Unity's DOTS sample against a ground-up C++ and OpenGL clone to measure the empirical trade-offs between an off-the-shelf engine and scratch-built code. (0:00)
  • The custom C++ engine handled an order of magnitude more projectiles than Unity DOTS, reaching roughly 420,000 bullets per volley before hitting the 100 ms frame time cutoff compared to Unity's ~44,000. (14:40)
  • Memory profiling revealed that Unity DOTS allocated approximately 500 bytes per bullet compared to just 40 bytes in C++, maintaining an overall RAM footprint roughly 8 to 9 times larger. (17:09)
  • Accounting for non-code editor overhead and GLSL debugging friction, building the game from scratch required an estimated 3.4 times the development effort of the Unity implementation. (25:59)

Detailed notes

## Background Published by Nick Caston on YouTube on December 18, 2019, this video compares Unity's official ECS/DOTS Angry Bots sample project against a scratch-built clone written in C++ and OpenGL. The project aims to provide concrete empirical data on runtime performance and development effort to inform the ongoing custom engine versus off-the-shelf engine debate.

## TL;DR Nick Caston benchmarks Unity's Data-Oriented Technology Stack (DOTS) against a custom-built C++ and OpenGL clone of the Angry Bots arena demo. By stressing both engines with extreme projectile volumes and continuous collision checks, the test reveals that the handbuilt engine delivers 4x to 10x better frame times and uses roughly 11% to 12% of the RAM consumed by Unity. However, Caston also formulates a development effort model incorporating code volume, GLSL debugging challenges, and editor workflows, showing that the C++ clone required roughly 3.4 times the engineering labor to build. He concludes that while a scratch engine offers strictly superior runtime performance and memory efficiency, implementing rendering pipelines and visual polish accounts for the vast majority of development time.

## Atmosphere & style The presentation is measured, analytical, and distinctly pragmatic, replacing ideological game-engine debates with clear-headed empirical data. Caston speaks with the grounded perspective of a practitioner who has directly grappled with low-level graphics code, candidly detailing the friction of debugging GLSL shaders by inspecting pixel colors. The narrative progresses smoothly from architectural theory and stress benchmarks into a thoughtful mathematical evaluation of developer time, leaving viewers with a balanced, unhyped appreciation for the real-world trade-offs between engine productivity and bare-metal control.

## Bulletin - [00:00](https://www.youtube.com/watch?v=tInaI3pU19Y&t=0s) Nick Caston benchmarked Unity's DOTS sample against a ground-up C++ and OpenGL clone to measure the empirical trade-offs between an off-the-shelf engine and scratch-built code. - [14:40](https://www.youtube.com/watch?v=tInaI3pU19Y&t=880s) The custom C++ engine handled an order of magnitude more projectiles than Unity DOTS, reaching roughly 420,000 bullets per volley before hitting the 100 ms frame time cutoff compared to Unity's ~44,000. - [17:09](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1029s) Memory profiling revealed that Unity DOTS allocated approximately 500 bytes per bullet compared to just 40 bytes in C++, maintaining an overall RAM footprint roughly 8 to 9 times larger. - [25:59](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1559s) Accounting for non-code editor overhead and GLSL debugging friction, building the game from scratch required an estimated 3.4 times the development effort of the Unity implementation.

## Key takeaways - [00:00](https://www.youtube.com/watch?v=tInaI3pU19Y&t=0s) The classic industry consensus holds that custom game engines offer superior runtime performance and control, but are rarely worth the substantial engineering investment. - [01:56](https://www.youtube.com/watch?v=tInaI3pU19Y&t=116s) Unity DOTS shifts away from traditional object-oriented GameObjects via three core pillars: the Entity Component System (ECS), the Burst Compiler, and the C# Job System. - [05:26](https://www.youtube.com/watch?v=tInaI3pU19Y&t=326s) In the Angry Bots sample, legacy GameObjects fail under high projectile loads, whereas the DOTS pipeline easily handles thousands of simultaneous bullets inside the Unity Editor. - [08:51](https://www.youtube.com/watch?v=tInaI3pU19Y&t=531s) To keep the comparison fair, the C++ AngryGL clone mimics Unity's constraints: no object pooling, discrete overlap collisions, and frame-rate-dependent simulation. - [10:16](https://www.youtube.com/watch?v=tInaI3pU19Y&t=616s) Minor simplifications were made in the C++ clone to save time, such as replacing the particle system with a sprite sheet and removing smooth follow dampening from the camera. - [11:59](https://www.youtube.com/watch?v=tInaI3pU19Y&t=719s) The benchmark methodology keeps four undying enemy bots in the scene to force continuous collision detection and terminates runs once frame time reaches the 100 ms firing interval. - [14:40](https://www.youtube.com/watch?v=tInaI3pU19Y&t=880s) While both implementations hit the 144 FPS display ceiling at low bullet counts, the custom C++ engine demonstrates a 4x to 5x performance lead in realistic envelopes and a 10x lead under extreme stress. - [17:09](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1029s) Under extreme entity volumes, Unity's CPU and GPU utilization collapse toward near-idle levels, suggesting the engine is bottlenecked by main memory and GPU bus transfers rather than raw compute. - [17:09](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1029s) Entity Debugger inspection indicates Unity DOTS uses roughly 200 bytes for component data per bullet, with an additional 300 bytes absorbed by culling structures, runtime tracking, and metadata. - [20:43](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1243s) To quantify development effort, a baseline metric assigns Unity's 542 lines of functional ECS C# code an editor multiplier of 2.0 to account for non-code work like scene and material setup. - [23:55](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1435s) The C++ implementation totaled 2,619 lines across C++, GLSL shaders, and build scripts, incurring extra development friction due to the difficulty of debugging shaders via pixel colors. - [25:59](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1559s) The resulting effort ratio of 3.4 indicates that building the project from scratch in C++ took roughly 3 to 4 times the labor of building it in Unity DOTS. - [25:59](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1559s) Most engineering hours on the custom engine were consumed by rendering features—dynamic shadows, bloom, and skeletal animation blending—rather than core gameplay mechanics or performance tuning. - [25:59](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1559s) Caston cautions that these empirical results reflect a small sample project and should not be treated as a universal rule across all game genres or team sizes.

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=tInaI3pU19Y&t=0s) Game Engines vs. Building From Scratch

Game engines provide tools designed to streamline development by handling common foundational systems—such as physics, lighting, animation, audio, player input, and cross-platform deployment to targets like Windows or Xbox. Despite these built-in systems, developers continually debate whether to use an established engine or write a game engine from scratch. The standard industry consensus suggests custom engines grant superior runtime performance and control, but are seldom worth the extra development time and effort.

Rather than settling for broad generalizations, Nick Caston set out to evaluate the concrete trade-offs between a modern engine and a scratch-built implementation. Focusing on Unity—a widely adopted, battle-tested platform spanning both indie and AAA productions—the experiment looks specifically at how Unity Technologies' recent push toward data-oriented architectures holds up against bare-metal code.

```mermaid flowchart TD accTitle: Custom Engine vs Off the Shelf Engine accDescr: Diagram contrasting traditional game engine trade-offs with custom scratch-built implementations. subgraph GE["Off-the-Shelf Engine (Unity)"] GE_Pros["Built-in Systems: Physics, Lighting, Cross-platform"] GE_Cons["Layers of Abstraction, Potential Overhead"] end subgraph Scratch["Custom C++ Implementation"] S_Pros["Maximum Control, Direct Memory Access, Higher Speed"] S_Cons["Heavy Development Effort, Reinventing Core Systems"] end GE -->|"Evaluate Trade-offs"| Comp["Empirical Performance & Effort Comparison"] Scratch -->|"Evaluate Trade-offs"| Comp ```

- Standard game engine responsibilities: physics simulation, dynamic lighting, skeletal animation, audio playback, input polling, and multi-platform compilation abstractions. - “The classic answer to this is yeah you can like you can build it yourself you know get better performance and you have more control over your project but it's usually not worth the extra effort,” remarks Nick Caston regarding the traditional dilemma.

---

[01:56](https://www.youtube.com/watch?v=tInaI3pU19Y&t=116s) Unity's DOTS and the Angry Bots Sample

Unity's Data-Oriented Technology Stack (DOTS) shifts the architecture away from traditional object-oriented programming (OOP). In standard OOP architectures, data is often hidden behind deep abstraction layers and scattered across heap memory. In contrast, data-oriented design focuses directly on memory layout and data transformation per tick or frame. Unity implements this philosophy across three primary pillars: the Entity Component System (ECS), the Burst Compiler (which compiles targeted C# subsets into highly optimized native machine code), and the C# Job System for multithreaded processing across available CPU cores.

```mermaid flowchart LR accTitle: Unity DOTS Architecture accDescr: The three core pillars that comprise Unity's Data-Oriented Technology Stack. subgraph DOTS["Unity DOTS Architecture"] ECS["Entity Component System (ECS)"] Burst["Burst Compiler"] Jobs["C# Job System"] end ECS -->|"Data Layout"| Jobs Jobs -->|"Parallel Work"| Burst Burst -->|"Native Code Generation"| Hardware["Multi-core CPU"] ```

To showcase the performance improvements of DOTS prior to its full release, Unity published an open-source sample project derived from *Angry Bots*. The demo features a top-down arena where a player character fights off spawning waves of worm-like robotic monsters using an automatic shotgun-style weapon. Inside the Unity Editor, the sample includes an architectural toggle allowing users to switch dynamically between the legacy GameObject/MonoBehaviour workflow and the DOTS/ECS implementation.

- The three pillars of Unity DOTS: - **Entity Component System (ECS):** Organizes program logic around raw data structs (components) and iterative logic (systems). - **Burst Compiler:** An LLVM-based compiler tailored for high-performance math and memory layout routines. - **Job System:** A safe multithreaded framework designed to saturate multi-core processors. - Player weapons use a `Spread Amount` parameter: firing triggers a grid of bullets equal to the spread value squared (e.g., a spread of 10 fires $10 \times 10 = 100$ bullets per shot). - Baseline firing rate: one volley every $0.1$ seconds ($100\text{ ms}$).

---

[05:26](https://www.youtube.com/watch?v=tInaI3pU19Y&t=326s) Testing Legacy Unity vs. ECS

In the sample running inside the Unity Editor, legacy object-oriented GameObjects handle 100 bullets per volley without immediate failure. However, increasing the spread value to 20 ($20 \times 20 = 400$ bullets per volley) causes severe lag, rendering the game unplayable whenever the trigger is held down.

Flipping the project's internal boolean toggle routes bullet and enemy spawning logic through the ECS pipelines instead. With the ECS path enabled, the game handles the previous 400-bullet workload effortlessly. Caston increases the spread parameter to 50—generating 2,500 bullets per shot fired ten times a second—and the scene continues to update smoothly within the editor without freezing, validating the raw architectural throughput of DOTS compared to classic Unity scripts.

Behind the simplistic gameplay loop, the project implements several visual and rendering details: - Dynamic shadows cast by both the player character and incoming enemies. - Weapon lighting effects, including instantaneous muzzle flashes that illuminate the player's 3D mesh. - Bloom/emissive post-processing, visible on the player's illuminated backpack fixtures. - Directional blend tree animations that transition the character between forward, backward, strafing, and diagonal walk cycles. - A smoothed follow-camera that glides and rotates slightly behind the player rather than snapping rigidly to the player's coordinates.

---

[08:51](https://www.youtube.com/watch?v=tInaI3pU19Y&t=531s) Project AngryGL: A Ground-Up C++ Clone

To test how Unity DOTS compares against custom code, Caston sets out to build an identical clone from scratch in C++ without relying on any third-party game engines. The target is to reproduce the full visual and mechanical fidelity of the Unity sample, including lighting, shadows, blending animations, and particle effects, rather than building a stripped-down prototype.

To keep the benchmark fair, Caston mirrors the deliberate design constraints Unity placed on the original project: 1. **No Object Pooling:** Bullets must be dynamically created and destroyed on demand rather than recycled from a pre-allocated pool. 2. **Frame-Rate Dependent Simulation:** Updates follow raw frame ticks rather than using a decoupled, fixed-timestep physics loop. 3. **Discrete Collisions:** Collision detection runs as discrete overlap checks instead of continuous ray sweeps.

```mermaid flowchart TD accTitle: AngryGL Architecture and Dependencies accDescr: Structural breakdown of the scratch-built C++ AngryGL project. subgraph AngryGL["AngryGL (Custom C++ Implementation)"] Core["Core C++ Codebase"] GL["OpenGL (Rendering)"] Window["Window Management Library"] Math["Matrix/Vector Math Library"] AssetLoad["Asset & File Format Parsers"] end Core --> GL Core --> Window Core --> Math Core --> AssetLoad ```

The scratch implementation relies on minimal dependencies: OpenGL for rendering, an OS window-management library, simple matrix/vector math libraries, and specialized asset parsers to read model and animation files.

- “I'm going to build a clone of that DOTS sample project and I'm going to build it from the ground up so I'm not going to use Unity or any other game engine I'm just going to start with a blank codebase and just make the game,” Caston states when detailing his methodology.

---

[10:16](https://www.youtube.com/watch?v=tInaI3pU19Y&t=616s) Inspecting the Handbuilt C++ Build

Caston presents the finished C++ clone running in real-time. The core gameplay and visuals match the Unity original: the character navigates the platform, enemies track the player, shotgun volleys fire on command, and directional animation blending transitions the character when running diagonally. Emissive bloom glows from the character's backpack, and the depth-based shadow mapping renders clean shadows across the scene.

Due to time constraints during development, Caston made two minor deviations from the original: - **Muzzle Flash:** Rather than writing an entire particle emitter system, the handbuilt version uses an animated sprite-sheet flipbook derived from screenshots of the Unity particle effect. - **Camera Follow:** The camera tracks the character's position directly on a rigid offset without the soft interpolation and rotational dampening found in the original.

These simplifications have negligible impact on raw processing and rendering throughput, though Caston notes they must be factored into the overall development effort calculations. To push the handbuilt engine's stress limits, Caston increases the spread amount to 50 ($2,500$ bullets per volley) and reduces the firing delay from $0.1$ seconds to zero, setting up an immediate stress test.

[11:59](https://www.youtube.com/watch?v=tInaI3pU19Y&t=719s) Benchmark Methodology and Hardware Constraints

To conduct a rigorous head-to-head performance analysis rather than just tinkering inside the editor, the testing environment requires strict constraints. Four undying enemy bots are spawned symmetrically around the player character; keeping them perpetually alive ensures that the spatial collision detection pipelines run continuously every frame without their computational overhead disappearing. In both the Unity DOTS sample and the C++ handbuilt clone, the player shoots in a single fixed direction at a static fire interval of 0.1 seconds (100 milliseconds). The `spreadAmount` variable is gradually scaled up, with the actual bullet count per shot scaling as the square of that number. Tests are stopped once the time required to render and process a single frame reaches the 100-millisecond fire interval. As Nick points out: “We're not framerate independent at all, so we can't actually fire bullets faster than frame rate allows.”

The primary measurement is frame time—the elapsed time between frames—expressed in milliseconds, which provides a cleaner, lower-is-better metric than raw frames per second (FPS) while correlating directly with hardware load. System resource utilization across RAM, CPU, and GPU is tracked using Windows Task Manager on an isolated per-process basis to avoid third-party software interference. Nick closes all non-essential applications during the benchmark runs and deliberately omits application startup times: without an upgraded Unity commercial tier, there is no way to bypass the mandatory splash screen on release builds.

```mermaid flowchart TD accTitle: Benchmark Methodology Flow accDescr: Diagram illustrating the experimental setup and loop used to benchmark Unity DOTS against the handbuilt C++ engine.

subgraph Setup["Test Scenario Setup"] A["4 Undying Enemies<br/>(Forces continuous collision)"] B["Fixed Shooting Direction<br/>(Interval = 0.1s / 100ms)"] end

subgraph Exec["Benchmark Iteration"] C["Increase 'spreadAmount'<br/>(Bullets = spreadAmount²)"] D["Measure Frame Time<br/>& System Resources"] end

subgraph Termination["Stopping Condition"] E{"Frame Time >= 100ms?"} F["Halt Test Run"] end

Setup --> Exec C --> D --> E E -- "No" --> C E -- "Yes" --> F ```

- Enemy targets: 4 enemies arranged evenly around the player, set to never die so collision detection runs constantly. - Firing interval: Exactly 0.1 seconds (100 ms) per burst in a uniform direction across both engines. - Test cutoff threshold: Benchmarking stops when average frame time hits or exceeds the 100 ms interval. - Resource tracking: RAM, CPU, and GPU monitored on a per-process basis via Windows Task Manager. - Omitted metrics: Engine startup time is excluded because the free Unity license forces the engine splash screen.

---

[14:40](https://www.youtube.com/watch?v=tInaI3pU19Y&t=880s) Frame Time Scalability: Unity DOTS vs. Handbuilt C++

Tracking frame times averaged over 100 frames reveals clear scaling tiers. On the test machine, the display and pipeline bottom out at 7 milliseconds (roughly 144 FPS), while 16 milliseconds marks the standard 60 FPS threshold where gameplay feels responsive and fluid. At low bullet densities, both Unity DOTS and the C++ implementation easily saturate the monitor's 144 FPS cap. However, as the entity counts expand exponentially into stress-test territory, their scaling curves diverge sharply.

| Spread Amount | Bullets per Shot | Unity DOTS Frame Time (ms) | Handbuilt C++ Frame Time (ms) | |---|---|---|---| | Low / Initial | Small counts | ~7 ms (144 FPS cap) | ~7 ms (144 FPS cap) | | Extreme scaling | ~44,000 | ~104 ms (Cutoff threshold) | Low / smooth | | Absolute ceiling | ~420,000 | N/A (Failed earlier) | ~100 ms (Cutoff threshold) |

Unity DOTS degrades steadily under heavy projectile loads, climbing through 27 ms and 72 ms until it breaches the unplayable 104 ms mark at approximately 44,000 bullets per shot. At that same volume, the custom C++ engine continues to run effortlessly. The handbuilt solution does not hit the 100 ms ceiling until firing approximately 420,000 bullets per burst—representing an order of magnitude increase in entity handling capacity. As Nick observes regarding real-world relevance: “They start off very similar, and the more extreme you go, the bigger of differences, with around a 10x difference up here, but in these realms that you're probably more likely to care about, it's more of a four to five times difference.”

- 7 ms represents the ~144 FPS display refresh ceiling of the benchmarking setup. - 16 ms represents the standard 60 FPS target for smooth real-time interactive gameplay. - Unity DOTS crosses the unplayable 100 ms threshold (~104 ms) at roughly 44,000 bullets per burst. - Handbuilt C++ clone maintains stability until roughly 420,000 bullets per shot before crossing the same limit. - Performance advantage of the custom engine ranges from identical at low counts, to 4x–5x in practical gameplay envelopes, up to 10x in extreme synthetic conditions.

---

[17:09](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1029s) CPU, GPU, and Memory Footprint Analysis

CPU and GPU tracking exposes the architectural bottlenecks limiting Unity DOTS. While the handbuilt engine maintains a flat, stable CPU utilization hovering around the mid-30% range throughout the test, Unity DOTS displays an initial rise followed by an aggressive plunge. This steep downward slope in CPU activity is mirrored by the GPU utilization, which drops precipitously toward near-idle levels right as frame times cross the 100 ms mark. This drop-off indicates that the Unity DOTS pipeline is not stalling on raw mathematical execution or shader compute. Instead, Nick deduces: “I think this is a pretty strong indication that the Unity version is not at all bound by GPU or CPU usage; I suspect that's probably something to do with I/O between main memory and the GPU memory.”

The RAM comparison demonstrates an even larger delta, maintaining a consistent ratio where the C++ engine consumes only 11% to 12% of the memory used by Unity. When graphed, the slope shows Unity allocating roughly 500 additional bytes of system RAM for every single bullet spawned, compared to just 40 bytes per bullet in the custom C++ engine. Inspecting a bullet entity inside Unity's Entity Debugger shows an assortment of component data fields and duplicate tracking metadata that total roughly 200 bytes per entity. The remaining ~300 bytes per bullet are likely absorbed by adjacent data structures—such as bounding volumes, KD-trees for spatial partitioning and frustum culling, garbage collector tracking references, or JIT runtime allocations:

- RAM consumption ratio: The custom C++ engine uses roughly 11%–12% of the RAM consumed by Unity DOTS (Unity uses ~9x more memory). - Per-bullet memory cost: ~500 bytes per bullet in Unity DOTS versus ~40 bytes per bullet in handbuilt C++. - Entity Debugger breakdown: Direct entity component values account for ~200 bytes in Unity DOTS; the remaining ~300 bytes stem from internal data structures, culling trees, or C# runtime/GC overhead. - Bottleneck behavior: Near-zero GPU and collapsing CPU utilization at high load demonstrate memory bus / dispatch bottlenecks rather than compute saturation.

---

[20:43](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1243s) Quantifying Development Effort: C# DOTS vs. C++ Engine

Evaluating raw engine performance addresses only half the equation; the engineering cost required to achieve that performance from scratch must also be calculated. Nick introduces a mathematical framework to evaluate the relative effort ratio:

$$\text{Effort Ratio} = \frac{E_{\text{HB}}}{E_{\text{Unity}}}$$

where $E_{\text{HB}}$ is the effort spent on the handbuilt C++ implementation and $E_{\text{Unity}}$ is the effort spent on the Unity DOTS project.

To establish an objective baseline, the unit of measurement is defined as the effort required to produce one average line of C# code in the Unity project. The total effort for Unity cannot be measured solely by raw source lines, because an extensive portion of a Unity title's behavior is configured visually through inspector windows, materials, lighting setups, animation state graphs, and scene hierarchies. Nick balances this by introducing a scalar multiplier of 2, doubling the weight of the raw line count to account for editor-side work.

To determine the baseline code size, Nick strips all non-ECS legacy fallback paths, comments, and blank lines from the Unity project while formatting its braces to match the C++ engine's layout. This results in exactly 542 lines of functional C# ECS code. In explaining why he set the editor overhead multiplier to two, Nick remarks: “There are a lot of elements to defining your Unity project that are outside the code, and from my personal experience, I've never worked on large-scale Unity projects, just a little small toy.”

- Baseline metric unit: The effort required to author one average line of C# code in the Unity project. - Unity ECS line count: Exactly 542 lines of clean C# code (excluding comments, blank lines, and non-ECS code paths). - Editor effort coefficient: Set to a factor of 2 to account for non-code assets, scene layout, animation graphs, and material creation. - Handbuilt model adjustments: The C++ effort calculation factors in a relative per-line difficulty coefficient and an additional modifier to compensate for minor omitted features.

[23:55](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1435s) Calculating Development Effort: Unity vs. Handbuilt

Nick Caston wraps up the estimation formula to quantify the relative development effort between the two implementations. For small toy projects like this one, spending roughly half the time on code and the other half on non-code assets and setup maps quite well to reality, bringing the total Unity effort metric to 1,084. Turning to the handbuilt C++ and OpenGL clone, the total codebase comes out to 2,619 lines of code once boilerplate is stripped away. This figure encompasses everything needed to build and run the project: the core C++ code, the build configuration scripts, and the GLSL shader code executed on the GPU.

```mermaid flowchart LR accTitle: Development Effort Estimation Formula accDescr: Diagram showing how lines of code, effort ratios, completion percentage, and non-code multipliers produce final effort values. subgraph Unity ["Unity DOTS"] U_LOC["Base Code Lines"] --> U_Calc["Effort Formula (LOC × Ratio × Multipliers)"] U_Calc --> U_Total["Unity Effort Value: 1,084"] end subgraph Handbuilt ["Handbuilt C++ / OpenGL"] H_LOC["2,619 Lines (C++, GLSL, Build)"] --> H_Boost["Line Effort Ratio (~1.25)"] H_Boost --> H_Comp["90% Completion Adjustment"] H_Comp --> H_Total["Handbuilt Effort Value: 3,638"] end U_Total --> Comp{"Relative Effort"} H_Total --> Comp Comp --> Result["3x to 4x More Effort for Handbuilt"] ```

When deciding on the line-effort ratio for the custom engine, Caston initially considered setting it around 2.0 due to the complex mathematical routines involved—such as manual ray tracing calculations or determining the player's aiming orientation from 2D mouse screen coordinates. However, examining Unity's ECS implementation revealed that ECS code is itself far from trivial, and much of the math in the C++ version naturally spreads across multiple lines. Nonetheless, he gave the handbuilt code a slight multiplier boost because debugging OpenGL and GLSL remains notoriously painful: “I've often found myself just rendering pixels of particular colors to try and figure out what the state was in the GLSL code which it does slow down development a bit.” Factoring in a 90% completion score to account for a few missing polish features, the final calculated effort value for the handbuilt version reaches 3,638.

- **Unity Effort Metric:** 1,084 points (incorporating roughly a 50/50 split between coding and non-code tasks). - **Handbuilt Code Volume:** 2,619 total lines (inclusive of C++, GLSL shader stages, and build scripts). - **Shader Debugging Overhead:** Lack of native inspection tools forced visual debugging via raw pixel color outputs. - **Handbuilt Completion Level:** Graded at 90% due to minor omitted features. - **Handbuilt Effort Metric:** 3,638 points.

---

[25:59](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1559s) Final Comparison and Key Takeaways

Dividing the handbuilt effort score by the Unity score yields a ratio of 3.4. Stripping away false precision, Caston summarizes that building this specific game from scratch in C++ and OpenGL demanded approximately 3 to 4 times the effort of creating it in Unity DOTS. He notes that this matches real-world intuition: an experienced Unity developer could assemble a toy project like this in about a single day, whereas a programmer comfortable with 3D math and modern C++ would likely take roughly a full week to get the custom version running.

```mermaid flowchart TD accTitle: Handbuilt Development Time Breakdown accDescr: Flowchart illustrating where time was spent during the development of the handbuilt engine. Root["Handbuilt Engine Effort"] --> Core["Core Game Mechanics & Optimization (Straightforward & Fast)"] Root --> Polish["Visual Polish & Graphics Pipeline (Consumed Most Development Time)"] Polish --> P1["Dynamic Lighting & Shadows"] Polish --> P2["Bloom Post-Processing"] Polish --> P3["Skeletal Animation Blending"] ```

When evaluating raw runtime results, the handbuilt solution delivered strictly superior performance across every tested benchmark, showing an especially dramatic advantage in memory footprint and notable reductions in frame time. However, that performance came at the cost of substantial labor. Most of the engineering hours on the handbuilt clone were consumed by visual polish—implementing lighting, dynamic shadows, bloom post-processing, and skeletal animation blending—rather than the gameplay loop: “getting the actual game mechanics working and getting it all optimized that was it was actually pretty straightforward.”

Caston closes by urging developers not to over-generalize these findings to every production scenario. “I think it's important just to give out a reminder that, you know, this is not a one-size-fits-all comparison for different projects, different teams—all these different variables are going to change this hugely,” he emphasizes, reiterating that this test serves merely as a single empirical benchmark before inviting viewers to share their feedback in the comments.

- **Effort Ratio:** Handbuilt required ~3x to 4x the labor of Unity DOTS (calculated strictly at 3.4x). - **Estimated Build Times:** ~1 day for an experienced Unity programmer versus ~1 week for a capable C++ developer. - **Performance Verdict:** Handbuilt achieved strictly better performance across memory utilization and frame times. - **Primary Time Sink:** Implementing rendering features (lighting, shadows, bloom, animation blending) took far longer than mechanics or optimization. - **Extrapolation Warning:** The result represents a single toy-project data point and should not be treated as a universal rule for all team sizes or game genres.

## Mentioned Nick Caston, Unity Technologies, Unity, Unity DOTS, Entity Component System (ECS), Burst Compiler, C# Job System, Angry Bots, AngryGL, C++, OpenGL, C#, GLSL, LLVM, Windows Task Manager, Xbox

## Video description Comparison between one of Unity's sample ECS/DOTS projects, and a "from scratch" cloned implementation using C++ and OpenGL. Bringing some data to the game engine discussions. Custom game engine vs Unity DOTS!

Unity sample project repo: https://github.com/UnityTechnologies/AngryBots_ECS

C++ clone source: https://github.com/ntcaston/AngryGL

You can watch me live stream game development at https://www.twitch.tv/nickcaston

OpenGL® and the oval logo are trademarks or registered trademarks of Hewlett Packard Enterprise in the United States and/or other countries worldwide.

"Unity", Unity logos, and other Unity trademarks are trademarks or registered trademarks of Unity Technologies or its affiliates in the U.S. and elsewhere.

The stylized “C++” logo is a trademark of the Standard C++ Foundation.

[Source](https://www.youtube.com/watch?v=tInaI3pU19Y)

中文简报

自研C++引擎性能超Unity DOTS十倍,但人力成本增加两倍 自研C++引擎的实体处理能力最高可达Unity DOTS的十倍,且内存占用减少近90%,但从零构建基础图形管线所需开发工作量增加了两倍以上。 Nick Caston将Unity的数据导向型技术栈(DOTS)与一个自研的C++及OpenGL版Angry Bots竞技场演示进行了基准测试对比。通过极限弹幕数量和持续碰撞检测对两款引擎进行压力测试,结果显示自研引擎的帧时间表现提升了4到10倍,且RAM占用仅为Unity的约11%至12%。然而,Caston结合代码量、GLSL调试难点以及编辑器工作流建立了一套开发工作量模型,结果表明C++版本的工程人力消耗约为Unity的3.4倍。他总结道,尽管从零打造的引擎在运行时性能和内存效率上展现出绝对优势,但实现渲染管线和视觉打磨却占据了绝大部分开发时间。 Nick Caston将Unity的DOTS示例与一个从零构建的C++和OpenGL克隆版本进行基准测试对比,以实测现成引擎与从零自研代码之间的权衡取舍。 (0:00) 自研C++引擎处理的弹幕数量比Unity DOTS高出一个数量级,在达到100毫秒帧时间截止线前单轮齐射可达约420,000发子弹,而Unity仅为约44,000发。 (14:40) 内存分析显示,Unity DOTS每颗子弹大约分配500字节,而C++仅为40字节,前者的整体RAM占用约为后者的8到9倍。 (17:09) 综合考虑非代码层面的编辑器开销和GLSL调试阻力,从零构建该游戏所需开发工作量约为Unity实现的3.4倍。 (25:59)

Reading summary

Nick Caston evaluated the trade-offs between commercial game engines and custom software by benchmarking Unity's Data-Oriented Technology Stack (DOTS) against a ground-up C++ clone of an Angry Bots prototype. His findings demonstrate that while a handbuilt engine delivers superior memory efficiency and a 4x to 10x throughput advantage, it demands roughly 3.4 times the development effort. Benchmark Architecture and Experimental Constraints (11:59) To evaluate Unity's data-oriented shift against bare-metal code, Caston mirrored Unity's open-source Angry Bots sample by building a clean-slate C++ and OpenGL clone called AngryGL. The sample features an automatic shotgun firing volleys that scale quadratically with a spread parameter at a fixed 0.1-second interval. While Unity's legacy GameObject workflow collapsed under a spread of 20 (400 bullets), its Entity Component System (ECS) pipeline handled 2,500 bullets smoothly in the editor. To isolate engine efficiency, AngryGL replicated Unity's exact constraints: dynamic entity allocation without object pooling, frame-dependent simulation, and discrete collision checks. The benchmark placed four immortal enemy bots symmetrically around the player, guaranteeing continuous spatial collision overhead. Both implementations fired in a uniform direction every 100 milliseconds, with tests scaling up bullet counts until average frame times breached the 100-millisecond fire interval. System resource usage was tracked through Windows Task Manager on an isolated per-process basis, with application startup times omitted due to mandatory engine splash screens on free Unity tiers. Frame Time Scaling and Memory Utilization (14:40) At low projectile densities, both Unity DOTS and the handbuilt engine saturated the 144 FPS display refresh ceiling (~7 ms frame time). As workloads escalated into stress-test volumes, performance diverged sharply. Unity DOTS exceeded the 100 ms cutoff at approximately 44,000 bullets per burst (averaging 104 ms), whereas the handbuilt C++ engine sustained interactive speeds until reaching approximately 420,000 bullets per burst. Caston observed that while extreme synthetic stress exposed a 10x throughput gap, the practical performance advantage in realistic gameplay envelopes hovered between 4x and 5x. Hardware tracking revealed severe memory and dispatch bottlenecks in Unity. The handbuilt engine consumed only 11% to 12% of the RAM used by Unity DOTS, allocating roughly 40 bytes per bullet compared to Unity's ~500 bytes. Inspection via Unity's Entity Debugger showed component fields accounted for ~200 bytes per entity, with the remaining ~300 bytes absorbed by bounding volumes, KD-trees for culling, and garbage collection or runtime metadata. Under heavy loads, Unity's CPU and GPU utilization dropped precipitously toward idle as frame times degraded, which Caston deduced was caused by memory bus and dispatch stalls between main system RAM and the GPU rather than compute saturation. Quantifying Development Effort and Labor Sinks (20:43) To assess whether performance gains justified the engineering cost, Caston formulated an effort metric indexed to authoring one average line of C# code. The Unity DOTS sample contained 542 lines of functional C# ECS code, which Caston scaled by an editor multiplier of 2 to yield 1,084 effort points, balancing code against non-code editor tasks such as scene graphs, lighting, materials, and animation trees. The handbuilt engine totaled 2,619 lines across C++, GLSL shaders, and build scripts. Applying a 1.25 multiplier for difficult low-level debugging—such as rendering raw pixel colors to inspect GLSL state—and adjusting for a 90% completion level produced a handbuilt score of 3,638. The resulting effort ratio showed that the handbuilt implementation required roughly 3.4 times the labor of the Unity project. Caston estimated this translates to roughly one day of work for an experienced Unity developer versus a full week for a competent C++ programmer. Crucially, writing gameplay logic and mathematical optimization in C++ proved straightforward; the bulk of development time was consumed by implementing foundational rendering features, including dynamic shadows, lighting, bloom post-processing, and skeletal animation blending. Caston concludes that while custom engines provide unmatched runtime efficiency and control, writing graphics pipelines from scratch rarely makes practical sense for most production teams. He cautions that these findings stem from a single, small toy project and should not be treated as a universal rule across different genres, team capabilities, or project scales.

精炼阅读摘要

Nick Caston 通过构建同款 C++ 原生克隆项目对比 Unity DOTS 发现,自研原生引擎在吞吐极限与内存效率上最高可取得 10 倍优势,但代价是付出约 3 到 4 倍的开发工作量。 架构演进与同等基准设计 (1:56) 传统面向对象架构(OOP)常因深层抽象和堆内存碎片限制运行效率,Unity 为此推出了面向数据技术栈(DOTS),由实体组件系统(ECS)、Burst 编译器以及 C# Job System 组成,旨在最大化榨干多核 CPU 算力。为了实测现代引擎的面向数据改造与裸机原生代码的真实差距,Caston 以 Unity 官方《Angry Bots》技术演示为参照,完全从零开始使用 C++ 与 OpenGL 构建了名为 AngryGL 的克隆项目,复刻了包括动态阴影、枪口光效、泛光后期以及方向混合树动画等完整图形表现。 为确保测试的公平性与可比性,C++ 原生项目严格复现了 Unity 演示的各项设计约束:禁止使用对象池技术(子弹必须即时动态创建与销毁)、模拟逻辑完全依赖原始帧率而非解耦的固定物理步长,并采用离散重叠检测替代连续射线检测。除了极少数无伤大雅的妥协(如将基于粒子的枪口火焰替换为序列帧图集、移除相机的平滑插值跟随),两套项目在系统逻辑和运算负载上保持了高度一致。 极限实体吞吐与帧时间断崖 (11:59) 基准测试在严格隔离的环境下展开:场景中固定保留 4 个不灭的敌对单位以迫使空间碰撞检测管线每帧持续执行,玩家以 0.1 秒(100 毫秒)的固定间隔沿单向开火,并通过指数级提升散射参数(发弹量为参数的平方)持续施压,单帧耗时达到 100 毫秒即判定为测试终止。在低密度负载下,两套方案均可轻松触及测试机 144 FPS(单帧约 7 毫秒)的显示刷新上限;然而当实体量级呈指数级扩张时,二者的伸缩性表现发生严重分化。 在传统 GameObject 模式下,发射 400 发子弹便会导致 Unity 严重卡顿;启用 DOTS ECS 后,承载能力大幅飞跃,但依然在单次爆发约 44,000 发子弹时帧时间达到 104 毫秒,突破了可玩性极限。相比之下,C++ 自研引擎在同等负载下运行极其平稳,直到单次爆发子弹量推升至约 420,000 发时才达到 100 毫秒的终止阈值。Caston 指出,在日常实际游戏区间内原生引擎拥有约 4 到 5 倍的性能优势,而在合成压力测试的极端工况下,实体承载量差距达到了 10 倍。 资源开销与硬件瓶颈根因 (17:09) 监控进程资源占用揭示了 Unity DOTS 在极限压力下的底层瓶颈。在测试全过程中,自研 C++ 引擎的 CPU 使用率平稳维持在 30% 中位区间;Unity DOTS 的 CPU 占用率却在初期冲高后出现剧烈暴跌,且 GPU 使用率也在帧时间突破 100 毫秒时同步滑落至接近闲置。Caston 据此推断,Unity DOTS 的性能崩溃并非受限于 CPU 数学运算或 GPU 着色器算力饱和,更大概率是主内存与显存之间的 I/O 调度和总线吞吐出现了严重滞塞。 内存层面的差距更为悬殊。在整个基准测试中,C++ 原生引擎占用的 RAM 仅为 Unity 的 11% 到 12%,意味着 Unity 的内存消耗达到了前者的约 9 倍。数据斜率表明,场景中每生成一颗子弹,自研引擎仅需分配约 40 字节系统内存,而 Unity DOTS 却要分配约 500 字节。尽管 Unity 实体调试器显示子弹组件本身的纯数据字段约占 200 字节,但剩余的 300 字节被包围体、空间划分树(如 KD-Tree)、视锥剔除结构、GC 跟踪引用及 JIT 运行时开销所占据。 开发成本量化与工程代价 (20:43) 单纯衡量运行时性能仅能展现工程选型的一半,自研代码所需投入的工时成本同样需要严谨评估。Caston 引入了一套量化评估模型:以在 Unity 中编写一行标准 C# 代码的工作量为基准单位,由于 Unity 项目大量依赖检查器面板、材质调优、动画状态图以及层级视图等可视化配置,因此对剔除注释和冗余后的 542 行核心 ECS C# 代码赋予 2 倍的编辑器工作量加权系数,得出 Unity 总投入分值为 1,084。 在自研端,包含核心 C++、构建配置脚本及 GPU 运行的 GLSL 着色器在内的有效代码总量为 2,619 行。考虑到缺乏专业检查工具导致调试 OpenGL 与 GLSL 异常繁琐(往往需要通过输出特定像素颜色来反查状态),Caston 设定了约 1.25 的代码难度倍率,并结合 90% 的功能完整度补正,计算出 C++ 自研方案的开发投入分值高达 3,638。自研分值除以 Unity 分值得出的工时比约为 3.4,意味着纯手工搭建此项目的综合工作量是使用 Unity DOTS 的 3 到 4 倍。 Caston 总结指出,自研 C++ 原生引擎在帧时间和内存占用上具有碾压性优势,熟练开发者可在约 1 天内搭完 Unity 原型,而构建 C++ 版本则需约 1 周,且大部分自研耗时集中于渲染管线(光照、阴影、后处理及骨骼混合动画)而非核心玩法。该结论基于单个小型演示项目得出,不同团队规模、技术储备和品类需求下的实际权衡将存在巨大差异。

Detailed notes — zh

## Background 该视频由开发者 Nick Caston 于 2019 年 12 月 18 日发布在 YouTube 频道上。视频的核心内容是将 Unity 官方的面向数据技术栈(DOTS/ECS)示例项目与作者使用 C++ 和 OpenGL 从零手写的复刻版本进行全方位对比,旨在用真实的实测数据为游戏开发社区中经久不衰的“商业引擎 vs 自研手写”争论提供客观参考。 ## TL;DR 作者通过完整复刻 Unity DOTS 示例项目中的玩法与图形特性,在受控环境下对两者进行了极限定量测试。在性能表现上,从零手写的 C++/OpenGL 版本全面碾压 Unity,单次射击承载子弹数上限提升至约 10 倍,单颗子弹内存开销缩减至 Unity 的约十分之一(40 字节 vs 500 字节),并避免了 Unity 在高负载下陷入的主内存与显存间的 I/O 吞吐瓶颈。然而在研发成本建模评估中,手写版本的实际工作量是 Unity 方案的 3 到 4 倍,且绝大部分时间被耗费在着色器调试、阴影、泛光与骨骼动画融合等画面润色上,表明极致的底层性能是以数倍的开发工期为代价的。 ## Atmosphere & style 整个视频呈现出极其严谨、务实且兼具极客幽默的技术评测基调。Nick Caston 摒弃了社区中常见的口水战与空泛说教,以冷静客观的工程思维一步步展示代码清洗、基准测试设计与数学建模推导。观看体验既硬核纯粹又趣味盎然,尤其当他调侃硬核玩家面对高帧时间的“怒退摔手柄”、吐槽 OpenGL 着色器调试的痛苦,以及深入 Entity Debugger 探寻 Unity 内存“黑洞”时,真实还原了一名底层程序员死磕性能与开发效率时的执着与理性。 ## Bulletin - [14:40](https://www.youtube.com/watch?v=tInaI3pU19Y&t=880s) 在极限弹幕负载测试中,Unity DOTS 在单次发射约 44,000 颗子弹时帧时间达到 104 毫秒的崩溃阈值,而手写 C++/OpenGL 引擎直到约 420,000 颗子弹才触及上限,极限性能差距达 10 倍左右。 - [18:01](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1081s) 内存效率对比悬殊,手写 C++ 版本内存占用仅为 Unity DOTS 的 11% 到 12%;Unity 单颗子弹带来约 500 字节内存开销,而手写版仅需约 40 字节。 - [17:09](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1029s) 高负载下 Unity 的 CPU 与 GPU 占用率双双发生断崖式下跌,表明其瓶颈并非算力或图形渲染不足,而是受限于主内存与 GPU 显存之间的数据 I/O 传输吞吐。 - [25:59](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1559s) 经数学建模与代码量量化折算,手写版本的综合研发精力约为 Unity 版本的 3 到 4 倍(分值比为 3638 对 1080),开发耗时大部分消耗在光照、阴影和动画混合等画面表现层。 ## Key takeaways - [00:00](https://www.youtube.com/watch?v=tInaI3pU19Y&t=0s) 行业长期认为从零自研游戏引擎虽能提升性能但投入产出比过低,本次实验旨在通过实测数据量化手写引擎相比 Unity DOTS 的实际性能差距与精力成本。 - [03:56](https://www.youtube.com/watch?v=tInaI3pU19Y&t=236s) Unity DOTS 依托 ECS、Burst 编译器和 C# Job System 转型为面向数据设计,但在官方示例中未开 ECS 时发射 400 颗子弹即出现严重卡死,开启后性能获得大幅跃升。 - [07:23](https://www.youtube.com/watch?v=tInaI3pU19Y&t=443s) 为保证测试基准公平,手写 C++/OpenGL 版本(AngryGL)严格遵守官方约束:禁用对象池、采用离散碰撞检测,并完整复刻了动态阴影、泛光(Bloom)及全向跑步动画混合等复杂渲染特性。 - [11:59](https://www.youtube.com/watch?v=tInaI3pU19Y&t=719s) 基准测试采用 100 帧滑动平均的帧时间作为核心指标,并以 4 个受控无敌敌人提供持续碰撞负载,单帧耗时达到 100 毫秒射击间隔即被视为不可玩终止阈值。 - [14:40](https://www.youtube.com/watch?v=tInaI3pU19Y&t=880s) 在常规负载区间,手写 C++ 引擎相比 Unity DOTS 维持着 4 到 5 倍的稳定性能优势,而在逼近极限工况时该差距进一步扩大至约 10 倍。 - [17:09](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1029s) GPU 监控显示 Unity 在超载时图形显卡几乎闲置,反映出数据在主内存与 GPU 显存间的搬运阻塞(I/O 瓶颈)导致处理器与显卡双双陷入空转等待。 - [18:01](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1081s) Unity 子弹实体包含极为冗余的组件结构与运行时开销,其单颗子弹 500 字节的内存增长远高于手写版数据结构实际所需的 40 字节。 - [20:43](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1243s) 开发成本评估模型统一以‘Unity 中编写一行标准 C# 的精力’为基准单位,并引入权重因子平衡材质配置、动画状态机和场景搭建等非代码工作量。 - [23:55](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1435s) 手写版本总代码量达 2619 行(含 C++、GLSL 与构建脚本),由于缺乏成熟调试工具,GLSL 着色器开发中的像素调试等问题显著拉高了开发难度系数。 - [25:59](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1559s) 综合代码量与调试复杂度折算,手写自研方案所需付出的工期是 Unity 的 3 到 4 倍,熟练开发者在 Unity 仅需约 1 天完成的项目手写需耗时约 1 周。 - [26:43](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1603s) 手写引擎最大的时间开销并非核心游戏逻辑与底层性能优化,而是阴影、泛光与骨骼动画混合等视觉呈现细节的从零搭建。 - [26:43](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1603s) 手写底层虽能在内存与吞吐性能上取得绝对碾压,但面对 3 至 4 倍的开发成本激增,开发者需结合自身项目类型与团队规模谨慎权衡,不应盲目得出普适定论。 ## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=tInaI3pU19Y&t=0s) 游戏引擎之争与 Unity DOTS 的诞生背景 在游戏开发领域,现成的游戏引擎为开发者提供了高度集成的工作流,负责解决物理模拟、光照、骨骼动画、音频、用户输入以及跨平台移植等大量通用底层问题,让开发者只需编写一次代码便能同时发布到 Windows、Xbox 等多个平台。然而,社区中始终盘旋着一个经久不衰的经典争端:开发者究竟是否真的需要引擎,还是完全可以从零手写自己的游戏?传统的行内共识往往是“你可以自己写,虽然能获得更高的性能和绝对的控制权,但这通常不值得耗费那份巨大的额外精力”。为了探究手写引擎的性能提升与额外开发成本究竟有多大差距,主讲人决定抛开抽象争论,用真实的数据和项目来进行量化对比。 测试选取的参照对象是市场上最流行、经过无数独立游戏和 3A 大作实战检验的商业引擎 Unity。Unity Technologies 近年推出了面向数据技术栈(DOTS, Data-Oriented Technology Stack),旨在改变以往面向对象编程(OOP)中将数据深埋在重重抽象与封装之后的做法,全面转向面向数据设计(DOD)。该模式摒弃了冗余的抽象层,直接聚焦于内存中的数据布局以及数据在每帧或每个时间步长中的演变。完整形态的 DOTS 主要由三大核心支柱构成: ```mermaid flowchart TD accTitle: Unity DOTS 三大核心组件架构 accDescr: 展示实体组件系统、Burst 编译器与任务系统协同工作的逻辑关系 subgraph DOTS["Unity DOTS 技术栈"] ECS["Entity Component System (ECS)<br/>数据与逻辑解耦的架构框架"] BURST["Burst Compiler<br/>高度优化的底层数学与内存编译器"] JOBS["C# Job System<br/>安全高效的多核 CPU 并行调度"] end ECS --> BURST BURST --> JOBS ``` 关于手写引擎与商业引擎的权衡,Nick Caston 一针见血地概括了行内的传统观点:“The classic answer to this is yeah you can like you can build it yourself, you know get better performance and you have more control over your project, but it's usually not worth the extra effort.” 为了推广这套仍处于预览阶段但即将正式发布的架构,Unity 官方在 GitHub 上开源了一套示例项目,向开发者展示 DOTS 的实际落地效果与性能潜力。 ### [03:56](https://www.youtube.com/watch?v=tInaI3pU19Y&t=236s) Unity DOTS 官方示例项目实测:从卡顿到飞起 打开 Unity 内的 DOTS 示例工程,玩家控制一名角色在平坦开阔的地面上奔跑,四周会随机刷新出一批不断扭动爬行并追踪玩家的怪物。角色手持一把霰弹式枪械,每次开火都会射出一组网格状排列的子弹。子弹发射数量由界面右侧的散射参数(Spread Amount)决定,其实际发弹量为该数值的平方;开火冷却设定为 0.1 秒(即每 100 毫秒开火一次)。 该工程内置了传统面向对象方式与实体组件系统(ECS)的切换开关。在未开启 ECS 的传统模式下,游戏运行表现呈现出断崖式下跌: | 散射参数设定 (Spread) | 每次开火子弹数 ($Spread^2$) | 运行状态与帧率表现 | |:--- |:--- |:--- | | **10** | 100 发 | 勉强可以运行,能够正常移动与射击 | | **20** | 400 发 | 彻底卡死崩溃,只要按住开火键画面便陷入停顿,完全不可游玩 | 面对 400 发子弹导致的严重性能瘫痪,Nick Caston 直言:“So pretty much unplayable in that state, ECS to the rescue.” 在 IDE 中将布尔值开关切为开启后,引擎便转向了完全基于 ECS 构建的代码分支,不仅子弹生成走 ECS,敌人的碰撞检测也交由系统集中并行处理。在开启 ECS 的状态下,即使把散射参数进一步拉升到 50(即单次开火瞬间倾泻 2,500 发子弹),游戏依然能在尚未编译优化的 Unity 编辑器环境中保持极其流畅的运行。 ### [07:23](https://www.youtube.com/watch?v=tInaI3pU19Y&t=443s) 细节盘点与“手搓引擎”克隆计划 虽然该示例项目机制直观,但其画面与渲染层面包含诸多容易被忽略的技术细节: - **阴影与光照系统**:主角与怪物脚下均有实时投射的动态阴影;子弹射出时带有照亮主角全身的枪口火焰(Muzzle Flash)与点光源光照。 - **发光后处理(Bloom/Emission)**:角色背部背包自带高亮光源,光线会形成真实外溢的泛光光效。 - **骨骼与动作混合**:角色在不同朝向跑动时具备多套骨骼动画,并且在转向斜向奔跑时会自动进行动画融合(Blend)。 - **摄像机平滑跟踪**:镜头并非死板僵硬地锁死在角色头顶,而是带有柔和的延迟过渡与轻微的角度旋转。 为了进行公平且硬核的对照实验,Nick Caston 制定了一个纯手工克隆计划。他宣布:“I'm going to build a clone of that DOTS sample project, and I'm going to build it from the ground up, so I'm not going to use Unity or any other game engine, I'm just going to start with a blank codebase and just make the game so to speak.” 为了保证对比的基准一致,手写版本必须遵守与 Unity 演示完全相同的约束条件: - **严禁对象池(Object Pooling)**:遵循 Unity 官方演示时的规则,子弹不采用对象池复用机制,而是真实地高频分配与销毁。 - **离散碰撞检测(Discrete Collisions)**:不采用连续碰撞检测,与原版保持逻辑一致。 - **不保证帧率无关性**:与原工程机制保持一致,简化时间步长计算。 - **极简第三方依赖**:采用现代 C++ 编写,图形接口选用 OpenGL,仅引入轻量的窗口管理库、基础模型/图像文件解析库以及矩阵数学库,拒绝任何高层引擎封装。 ### [10:16](https://www.youtube.com/watch?v=tInaI3pU19Y&t=616s) C++ 手写克隆成果展示与取舍 经过一段时间的编写,Nick Caston 成功展示了名为 AngryGL 的手工复刻版本。在画面表现上,主角在平地上移动、怪物群集追逐并倾泻弹幕的核心玩法已被完整重现。得益于定制的着色器通道,其动态阴影质量甚至比原版更加清晰锐利;背包处的辉光扩散(Bloom)与发光材质同样得到了还原,角色的全向奔跑动画融合也顺畅运作。 由于开发工期有限,他在克隆过程中进行了两处不影响核心性能测量的工程妥协: - **枪口火焰粒子替代**:没有编写独立的 GPU 粒子系统,而是直接截取 Unity 中的粒子帧画面制作成精灵图表(Spritesheet)进行循环播放。 - **摄像机视角**:省去了平滑插值过渡逻辑,采用了严格锁定主角位置的跟随视角。 在完成基础验证后,Nick Caston 尝试在手写版本中测试极限压力条件。他将散射参数同样拉满至 50(即每次单发 2,500 颗子弹),更激进的是,他将原本 0.1 秒的开火间隔直接设为 0 秒(即没有任何射击冷却限制),准备在这个自建的 C++/OpenGL 底层架构上压榨出极限的算力与弹幕吞吐量。 ### [11:59](https://www.youtube.com/watch?v=tInaI3pU19Y&t=719s) 性能对比测试的设计规范与受控环境 在初步尝试让游戏每帧都开火并发现它依然能够平稳运行后,Nick Caston 决定停止随意折腾(“mucking around”),制定一套严谨规范的性能基准测试方案。为了公平对比 Unity DOTS 与手写 C++/OpenGL 实现,他在测试场景中放置了 4 个均匀分布在玩家周围的敌人。之所以必须放置敌人,是为了让碰撞检测算法持续全负荷运行,从而将这部分开销计入整体性能;同时,这些敌人被设定为永远不会死亡,以保证每一帧、每轮测试的受控条件完全一致。在测试过程中,角色始终朝固定方向射击,开火间隔严格保持在 0.1 秒(即 100 毫秒)。 Nick 采取逐步增大 `spread_amount`(子弹散射量)变量的方法,在每次递增后记录当前的性能表现。测试的终止阈值设定为:当单帧的处理时间达到或超过 100 毫秒的射击间隔时停止。这是因为该程序并非与帧率解耦(not framerate independent),无法以超过帧率允许的频率生成子弹。为了确保硬件指标不被外部因素污染,他在测试期间关闭了电脑上几乎所有其他后台程序,甚至放弃了录制屏幕。同时,由于没有购买 Unity 专业版授权来移除发布版本(Release build)中的启动闪屏界面(Splash screen),他无奈放弃了对应用启动时间(Startup time)的测量。 - **射击参数与控制条件**:开火间隔恒定为 0.1 秒(100 毫秒),每次朝固定单一方向射击。 - **碰撞负载**:玩家周围均匀排列 4 个无敌敌人,确保物理/碰撞逻辑持续计算。 - **终止条件**:当单帧耗时增加到 100 毫秒(处理单帧所需时间追平射击间隔)时停止测试。 - **排除的指标**:受限于 Unity 个人版的强制启动闪屏,启动耗时未列入测试范围。 --- ### [13:27](https://www.youtube.com/watch?v=tInaI3pU19Y&t=807s) 核心评估指标定义:帧时间与硬件利用率 Nick 明确了本次评测的核心度量衡。首先是帧时间(Frame time),即游戏处理相邻两帧之间所流逝的物理时间,取连续 100 帧的平均值。相比大众常讨论的每秒帧数(如 60 FPS 或 144 FPS),Nick 选择直接使用帧时间,原因在于它不仅直观,而且与 RAM、CPU 等资源占用指标在逻辑上完全契合——均为“数值越低越好”(Lower is better)。 在系统资源占用方面,Nick 借助 Windows 任务管理器(Task Manager)来独立监测各个进程对 RAM、CPU 以及 GPU 的占用率。他强调,直接使用任务管理器能够纯粹基于单一进程粒度展开观察,有效避免其他后台软件互相干扰。针对帧时间,Nick 设定了几个关键硬件基准点:在他的硬件环境下,测试的物理瓶颈极限约为 7 毫秒(约合 144 FPS);另一个关键参考点是 16 毫秒(约合 60 FPS),这是绝大多数玩家能够接受且感觉流畅的黄金标准。Nick 调侃道,虽然硬核玩家(hardcore gamers)可能对 60 FPS 嗤之以鼻,但对大多数普通人来说已经足够丝滑。 ```mermaid flowchart LR accTitle: 性能度量指标与判定基准 accDescr: 展示测试所采用的帧时间与硬件利用率两大指标及具体判定条件 subgraph FrameMetrics["帧时间指标 (Frame Time)"] A["100 帧滑动平均值<br/>(越低越好)"] --> B{"关键判定节点"} B -->|"~7 ms"| C["144 FPS<br/>硬件测试上限"] B -->|"~16 ms"| D["60 FPS<br/>主流流畅标准"] B -->|">= 100 ms"| E["单帧耗时超标<br/>终止测试阈值"] end subgraph ResourceMetrics["硬件资源占用 (Task Manager)"] F["内存 (RAM)"] G["处理器 (CPU)"] H["显卡 (GPU)"] end ``` --- ### [14:40](https://www.youtube.com/watch?v=tInaI3pU19Y&t=880s) 帧时间测试结果:极端负载下的悬殊差距 实测数据显示,两套实现在轻量负载与超高负载下的表现走向完全不同。测试数据表列出了 4 行指标:第一行是散射系数(`spread_amount`),第二行是该数值的平方(即每次射击实际生成的子弹实体数),后两行则是 Unity 与手写 C++ 版本在 100 帧平均下的帧时间。在子弹实体数量较低的阶段,两套方案均能轻松跑满硬件上限(维持在 7 毫秒左右)。然而,随着子弹数目暴增,Unity 版本迅速显露疲态。 当单次射击生成的子弹数达到约 44,000 颗时,Unity 版本的单帧耗时直接飙升到了 104 毫秒,彻底越过了 100 毫秒的不可玩阈值。Nick 针对这组数据吐槽道: > “we hit that threshold of 104 milliseconds which is which is really long it's not playable at all even 72 is probably not very playable 27 some people might be ok with it a bunch of people will rage quit but you can see we hit that limit at around 44 thousand bullets per shot” 此时手写引擎在 44,000 颗子弹的压力下依然运行得相当轻松平稳;手写 C++ 引擎直到单次射击子弹量逼近约 420,000 颗时,才最终触碰极限。Nick 分析图表指出,虽然在普通开发者真正关心的常规场景范围内,两者的性能差距大约在 4 到 5 倍之间,但在走向极限负载的过程中,手写版本与 Unity DOTS 版本的性能差距拉大到了整整 10 倍左右。 | 散射参数 (`spread_amount`) | 实际发射子弹数 (Entities) | Unity 帧时间 (ms) | 手写 C++ 帧时间 (ms) | 体验状态评述 | |:--- |:--- |:--- |:--- |:--- | | 极低负载 | 少量子弹 | ~7 ms | ~7 ms | 两者皆跑满 144 FPS 硬件上限 | | 中等负载 | 适量递增 | 上升至 27~72 ms | 稳定在极低水平 | 部分玩家尚可忍受,硬核玩家直接摔手柄退游 | | 临界阈值 (~210) | ~44,000 | 104 ms | 依然流畅巡航 (peering along nicely) | Unity 触发 100 ms 终止测试红线 | | 极限工况 (~648) | ~420,000 | 严重崩溃 / 无法测量 | 触碰极限 | 手写引擎达到承载上限 | --- ### [17:09](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1029s) CPU 与 GPU 占用:Unity 的 I/O 吞吐瓶颈 CPU 利用率图表展现了完全不同的行为模式。手写 C++ 引擎的 CPU 占用率全程非常平稳,基本稳定在 35% 左右的区间。相反,Unity 版本的 CPU 占用呈现出一种反常的波动:起步阶段较低,随后迅速飙升激增,紧接着在高负载下出现了一条陡峭的断崖式下滑轨迹。 更惊人的是 GPU 的利用率曲线。Unity 版本的 GPU 占用率在中后段直接“坠入深渊”(“pretty much just falls off a cliff”),在接近帧时间极限时,GPU 几乎处于完全闲置(idle)的状态。这一现象有力地证明了 Unity 版本的瓶颈既非算力受限(CPU bound),也非图形渲染受限(GPU bound)。Nick 据此作出了明确推断: > “and I think this is a pretty strong indication that the unity version is not at all bound by GPU or CPU usage I suspect that's probably something to do with IO between main memory and the GPU memory” 这种由于主内存与 GPU 显存之间数据传输吞吐(I/O)卡死导致的停顿,使得 CPU 和 GPU 都在空转等待数据同步,造成了计算资源的严重浪费。 --- ### [18:01](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1081s) 内存消耗剖析:每颗子弹 500 字节 vs 40 字节 在内存(RAM)使用量的对比上,Nick 戏谑地对观众说“希望你喜欢看数字,因为数据全在这里了”。测试结果呈现出极其悬殊的对比:手写 C++ 项目的内存消耗仅占 Unity DOTS 版本的 11% 到 12% 左右。更关键的是,两者的内存增长斜率相差巨大。Unity 每增加一颗子弹,系统整体内存开销便暴增约 500 字节;而手写版本每颗子弹增加的系统内存消耗仅为约 40 字节,这与手写代码内部为子弹结构体分配的精确内存大小完全吻合。 为了探寻 Unity 多出来的 500 字节究竟去了哪里,Nick 打开了 Unity 的 Entity Debugger 进行深度排查。他在单颗子弹实体上看到了极为臃肿繁杂的组件数据堆砌(“a smorgasbord of data stored here”),不仅字段繁多,甚至还存在部分数据冗余。即使用户代码层面的数据类型满打满算占去 200 字节,依然有 300 字节不知所踪。Nick 推测,这很可能是底层配套的数据结构开销(例如用于快速视锥剔除渲染实体的 KD-Tree 等空间划分结构),同时 Unity 内置的内存 Profiler 也没有给出足够的下钻分析依据。此外,C# 运行时本身也是不可忽视的内存放大器,包括垃圾回收引用(GC references)或者向即时编译器(JIT)传递数据时的额外系统开销。 总结所有的基准测试: - **帧时间表现**:从低负载下的毫无差距,拉大到极端工况下手写版本取得整整 10 倍的领先。 - **处理器与显卡利用率**:在流畅运行期两者利用率接近;在超载后 Unity 陷入 I/O 瓶颈,CPU 与 GPU 均双双骤降。 - **内存消耗比率**:Unity 运行时的内存消耗是手写 C++ 实现的约 9 倍(手写版仅占其 11%~12%)。 --- ### [20:43](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1243s) 开发成本建模:如何量化从零手写的代码代价 在展示了手写版本的性能优势后,Nick 将视线转向等式的另一面:脱离现成游戏引擎从零构建这套系统,到底需要多付出多少工作量?究竟是 2 倍、还是 5 倍?为了量化这种付出,Nick 建立了一个数学比率模型: $$\text{Effort Ratio} = \frac{E_{\text{HB}}}{E_{\text{Unity}}}$$ 其中 $E_{\text{HB}}$ 代表手写版本(Hand-built)的总开发精力,而 $E_{\text{Unity}}$ 代表 Unity 版本的总开发精力。由于 Unity 官方示例并非他本人编写,这里不可避免需要引入一些合理假设。为了使两者具有可比性,必须统一评估单位。Nick 决定将基准度量单位定义为:**在 Unity 项目中编写一行平均质量的 C# 代码所付出的精力**。 对于 Unity 版本的总开发代价模型,Nick 设立了两个核心变量相乘:第一个是 Unity 项目中纯粹的 C# 代码行数;第二个则是用于平衡非代码工作的加权系数(括号中的因子)。他指出,Unity 项目的大量核心逻辑并非写在代码里,材质调整、动画状态机(Animation graphs)、光照烘焙、场景搭建布局等大量工作都发生在编辑器交互中。 ```mermaid flowchart TD accTitle: 开发精力对比数学模型 accDescr: 说明手写与Unity版本开发精力的计算变量与折算因子 subgraph UnityEffort["Unity 成本评估 (E_Unity)"] U1["清洗后 C# 代码行数<br/>(542 行)"] -->|"相乘"| U2["编辑器非代码精力因子<br/>(权重系数 = 2)"] U2 --> U_Total["E_Unity = 1,084 单位"] end subgraph HBEffort["手写版本成本评估 (E_HB)"] H1["手写 C++ 代码行数"] -->|"折算"| H2["C++ 相对 C# 编写难度因子"] H2 -->|"修正"| H3["缺失特性补全补偿因子"] H3 --> H_Total["E_HB 总精力单位"] end U_Total -->|"比值运算"| Ratio["开发精力比率 (Effort Ratio)"] H_Total -->|"比值运算"| Ratio ``` 在采集 Unity 的实际代码行数时,Nick 做了极其严格的代码清洗:剔除所有空白行与注释,删掉非 ECS 的冗余代码分支,甚至把所有的花括号排版风格统一调整为与手写 C++ 代码一致的规范。清洗后统计出的核心 C# 代码行数恰好为 542 行。而针对非代码工作量因子,Nick 结合自身在轻量级原型开发中的经验,将其设定为 2,这意味着在引擎中完成美术资源配置、场景构建及编辑器配置所花费的精力,被估算为纯写代码本身的整整两倍。 ### [23:55](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1435s) 自研版本的代码量与开发工作量评估 在评估 Unity 项目的开发工作量时,作者指出对于类似这种小型实验性项目,通常大约一半的时间花在编写代码上,另一半时间则花在非代码相关的工作上。结合非代码因数的修正,Unity 版本最终的工作量数值计算为 1080。随后,作者将同样的评估公式套用到从零手写的 C++/OpenGL 克隆版本上:该项目的总代码量为 2619 行,且涵盖了项目中的所有内容,包括 C++ 代码、运行在 GPU 上的 OpenGL 着色器语言 GLSL 代码,以及构建配置文件。 关于“单行代码工作量比率”(line effort ratio),作者最初打算将其设定为 2 左右的高数值,理由是手写方案需要自己处理复杂数学计算,例如自己实现光线投射(ray tracing)或根据鼠标坐标推算角色瞄准朝向。然而,在查看 Unity 代码后,作者发现 Unity 中的 ECS 代码同样并非轻而易举,而且手写版本中很多复杂的数学计算本身就分散在很多行代码中,行数统计已经体现了这部分复杂度。不过,作者最终还是适当调高了手写版本的系数,主要原因在于 OpenGL 和 GLSL 的调试极其繁琐痛苦。Nick Caston 坦言:“I've often found myself just rendering pixels of particular colors to try and figure out what the state was in the GLSL code which it does slow down development a bit.” 此外,由于手写版本缺少了少数非核心要素,完成度按 90% 计,最终折算出手写版本的工作量数值为 3638。 - Unity 版本工作量评估值:1080 - C++/OpenGL 手写版本总代码行数:2619 行(含 C++、GLSL 着色器代码及构建脚本) - 手写版本功能完成度:90% - C++/OpenGL 手写版本最终工作量评估值:3638 ```mermaid flowchart TD accTitle: 工作量评估结构对比 accDescr: 对比 Unity DOTS 与手写 C++/OpenGL 的代码量、调试成本与最终工作量得分。 subgraph Unity["Unity DOTS 版本"] U_Code["代码时间 (~50%)"] U_NonCode["非代码配置/编辑器时间 (~50%)"] U_Score["最终工作量分值: 1080"] U_Code --> U_Score U_NonCode --> U_Score end subgraph Handbuilt["手写 C++/OpenGL 版本"] H_LOC["代码量: 2619 行 (C++, GLSL, 构建脚本)"] H_Debug["调试额外开销 (GLSL着色器调试繁琐)"] H_Comp["功能完成度: 90%"] H_Score["最终工作量分值: 3638"] H_LOC --> H_Score H_Debug --> H_Score H_Comp --> H_Score end ``` ### [25:59](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1559s) 开发工作量对比:3 到 4 倍的差距 将手写版本的工作量数值(3638)除以 Unity 版本(1080),得出的相对工作量比率为 3.4。去除过度精确的虚假精度后,作者得出结论:构建这款特定游戏的手写版本所需工作量,大约是 Unity 版本的 3 到 4 倍。 这种量化结果在直觉上也是完全合理的。作者给出了一个直观的工期对比:一个熟练的 Unity 开发者大概只要一天时间就能完成该项目;而如果是熟悉相关数学知识、精通 C++ 的开发者从零手写,大概需要一周左右的时间,而且还能剩下一点富余时间。 | 评估维度 | Unity (DOTS/ECS) | 手写克隆版 (C++ / OpenGL) | |:--- |:--- |:--- | | **计算工作量分值** | 1080 | 3638 | | **相对工作量比率** | 1.0x(基准) | 约 3.4x(约 3 ~ 4 倍) | | **熟练开发者耗时估计** | 约 1 天 | 约 1 周(仍有少许富余时间) | ### [26:43](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1603s) 总结:性能收益与研发成本的权衡 最后,作者对手写方案与 Unity DOTS 方案的对比进行了全面总结。在客观测试指标下,手写版本的性能表现展现出绝对优势。Nick Caston 总结道:“In conclusion the hand-built solution for this game provides strictly better performance for the measurements that we did with a very significant difference for the memory usage and potentially significant difference for the framerate or frame time.” 手写方案在内存占用上优势极为显著,在帧率和帧时间上也展现出极具潜力的差异。 然而,手写方案所需付出的工作量代价同样巨大。作者强调,手写版本耗时最多的地方根本不是核心游戏机制的实现或底层性能优化——这两部分其实相当直接了当;真正消耗海量研发时间的,是光照、阴影、泛光(bloom)以及动画混合(animation blending)等画面打磨与润色特性。作者在结尾处特别提醒,这项对比绝不是一个“一刀切”的普适结论,项目类型、团队规模和成员背景各不相同,这些变量都会极大地改变结论,因此这只是一个具体的单一参考数据点,观众切勿盲目进行过度推演。 ## Notable quotes > [26:43](https://www.youtube.com/watch?v=tInaI3pU19Y&t=1603s) In conclusion the hand-built solution for this game provides strictly better performance for the measurements that we did with a very significant difference for the memory usage and potentially significant difference for the framerate or frame time. — Nick Caston > [00:00](https://www.youtube.com/watch?v=tInaI3pU19Y&t=0s) The classic answer to this is yeah you can like you can build it yourself, you know get better performance and you have more control over your project, but it's usually not worth the extra effort. — Nick Caston ## Mentioned Unity, Unity Technologies, Unity DOTS, Entity Component System, Burst Compiler, C# Job System, Nick Caston, AngryBots_ECS, AngryGL, OpenGL, C++, GLSL, Windows, Xbox ## Video description Comparison between one of Unity's sample ECS/DOTS projects, and a "from scratch" cloned implementation using C++ and OpenGL. Bringing some data to the game engine discussions. Custom game engine vs Unity DOTS! Unity sample project repo: https://github.com/UnityTechnologies/AngryBots_ECS C++ clone source: https://github.com/ntcaston/AngryGL You can watch me live stream game development at https://www.twitch.tv/nickcaston OpenGL® and the oval logo are trademarks or registered trademarks of Hewlett Packard Enterprise in the United States and/or other countries worldwide. "Unity", Unity logos, and other Unity trademarks are trademarks or registered trademarks of Unity Technologies or its affiliates in the U.S. and elsewhere. The stylized “C++” logo is a trademark of the Standard C++ Foundation. [Source](https://www.youtube.com/watch?v=tInaI3pU19Y)

Source quotes

[26:43] Nick Caston: In conclusion the hand-built solution for this game provides strictly better performance for the measurements that we did with a very significant difference for the memory usage and potentially significant difference for the framerate or frame time. [0:00] Nick Caston: The classic answer to this is yeah you can like you can build it yourself, you know get better performance and you have more control over your project, but it's usually not worth the extra effort.

Original transcript

[0:00] Hey so recently I've been working on a [0:03] little project in the world of game [0:07] development and I thought the results [0:09] were pretty interesting and I figured [0:11] out I'd share with anyone who was [0:12] interested so in the world of game [0:15] development we have these game engines [0:17] which a tool that you can use which try [0:21] to streamline order of game development [0:23] and take care of a lot of things that [0:25] are common to many games so for example [0:27] they'll help you deal with your physics [0:29] and lighting animation audio handling [0:32] their user input and also helping you [0:35] launch to multiple platforms so you can [0:38] develop your game once and with [0:40] hopefully minimal changes you can add [0:42] that same game right on on Windows and [0:45] you know Xbox or what-have-you but [0:48] there's a common question that comes up [0:50] in game development very very common [0:52] question which is do I do I really need [0:56] a game engine or can I just build this [0:58] game myself and the classic answer to [1:02] this is yeah you can like you can build [1:03] it yourself you know get better [1:05] performance and you have more control [1:06] over your project but it's usually not [1:09] worth the extra effort and that sounds [1:11] pretty reasonable right like I just [1:12] listed off a whole bunch of features [1:14] that the game engine kind of takes care [1:17] of for you but I wanted to dig into this [1:19] a little bit more like this the speed of [1:23] performance and this extra if it how [1:25] extreme are those really how much more [1:27] if it is it and and how much performance [1:29] difference can we really expect so I'm [1:33] not trying to give the other the [1:34] definite of answer to this I'm just [1:37] trying to get give some numbers at least [1:38] some something to gauge off so the game [1:41] engine I'm going to be looking at is [1:43] unity which is one of the most popular [1:46] game engines in the market if not the [1:48] most popular game a huge number of games [1:50] are being built on it both indie and [1:52] triple A's so it's it's very very battle [1:54] tested it's it's a well proven game [1:56] engine and recently the folks working at [1:59] Unity Technologies have been making a [2:02] big push for performance with this [2:05] data-oriented tech stack [2:07] which the the short story is basically [2:10] they're moving away from the sort of [2:12] object-oriented style of development [2:14] where your your data is kind of hidden [2:17] Asst implementation details behind [2:19] layers of abstraction oftentimes [2:22] sometimes many layers of abstraction and [2:24] data oriented style of coding is [2:26] basically trying to remove a lot of [2:28] those layers and just directly thinking [2:30] okay what is my data and how is it [2:33] changing every every frame or every time [2:36] step or or what have you and so there [2:40] there are three components to dots the [2:42] first which is kind of the most obvious [2:44] one people think of and sometimes the [2:46] use of terms interchangeably is the [2:48] entity component system or ECS which is [2:51] kind of how you the the framework within [2:54] which you develop your your code and [2:57] this data oriented style is also the [3:00] burst compiler which is a new compiler [3:02] developing which plays nice with a kind [3:04] of data oriented style of writing code [3:07] the way you do memory allocations things [3:09] like that and the job system to help run [3:13] your code on multiple cores and really [3:15] leverage the the full CPU and so doctors [3:19] technically still in preview [3:21] they have been encouraging developers [3:23] use it for it for quite a while this so [3:24] they must have at least a reasonable [3:26] amount of faith in it and it is planned [3:28] for a full release very soon [3:30] early next year and so they've released [3:34] a sample projects publicly available [3:37] it's just a on github to kind of demo [3:40] dots to show how you might actually [3:43] implement some of the stuff in practice [3:45] and it also provides some some [3:48] motivation for why we should even care [3:50] about implementing our projects with [3:53] dots in unity so let's take a look at [3:56] that now so here we are in unity looking [4:00] at the dots or data oriented tech stack [4:03] sample project and you can actually [4:06] configure it to use dots or the ACS [4:09] system you can talk with that on and off [4:11] we'll leave it off for now yeah we'll [4:13] just show you the general gist of their [4:17] game so you play is this let's do down [4:20] here [4:21] and you just run around this lodge sort [4:25] of flat surface and these little Wiggly [4:29] monster dudes will start spawning [4:31] randomly and they will just chase after [4:34] you so as you move around no track after [4:37] you and then you've got this gun which [4:39] every shot shoots like grid of bullets [4:45] and the number of bullets that you fire [4:49] with every shot is controlled by this [4:50] spread amount over on the right here so [4:54] currently it's set to 10 which means [4:56] that will fire that number squared so [5:00] shoot 10 by 10 grid of bullets so you [5:03] end up with a hundred bullets per shot [5:05] and we got a mechs firing rate as well [5:08] Oh point one so that's we can fire once [5:12] every point one seconds or 100 [5:14] milliseconds so that would be if we were [5:17] to pass we can fire so we're still not [5:26] using ECS but let's just see what [5:29] happens when we increase this number so [5:31] remember that squared so we just went [5:33] from 100 bullets per shot up to 400 [5:36] bullets per shot and let's see what [5:39] happens when we do this and you'll see [5:44] that the the game pretty much just legs [5:47] out completely and it's pretty much [5:50] unplayable you just try to even shoot [5:52] once and if you hold it down it's just [5:55] it's terrible [5:56] [Music] [5:58] so pretty much unplayable in that state [6:02] ECS to the rescue so we'll just use this [6:06] little toggle here and it's not really [6:09] anything too magical so we'll just take [6:10] a look in my fancy IDE that just toggles [6:15] this boolean here from false to true and [6:17] it just sends you down different code [6:20] paths so it will just spawn bullets [6:22] using this entity component system which [6:26] you can grab the code yourself from [6:29] github but basically it just uses this [6:31] new system [6:33] and we also have to do that for the [6:36] enemies so they collide with each other [6:38] properly and so remember at 20 bullets [6:41] we were completely legging out but now [6:43] we've turned on ECS and gone down that [6:46] code path and if we take a look and we [6:57] can even increase the spread amount [7:00] probably not that much 50 so we're at [7:03] 2500 bullets per shot and it'll serve [7:12] remember we're in the we're in the unity [7:14] editor at the moment so this is not a [7:16] optimized release build of the game okay [7:20] so I'll see ya it's pretty well now well [7:23] the gameplay of this project is actually [7:24] pretty simple there's actually quite a [7:27] few subtleties going on here which are [7:29] easy to overlook so for example we've [7:33] got things like the shadow of the player [7:35] character and the shadow of the the [7:37] monsters living around we also have the [7:42] lighting from these bullets so you'll [7:44] see that the player getting close so [7:47] we've got this muzzle flash shoots the [7:53] whole character also got this bloom [7:57] effect so you can see it's easy to see [8:00] from his back it's got this backpack [8:02] with lights on it and you can see the [8:04] lighting kind of those clothes actually [8:07] bleeds out from the backpack that's kind [8:10] of it's called bloom you can call it a [8:11] mission if you like and we also have [8:15] animation so as the character moves [8:19] around here's different animations for [8:20] different directions will also emerge [8:24] between them so have you run forward to [8:26] the left go like this [8:29] okay a little bit dodgy so have you run [8:31] forward to the right it's pretty weird [8:32] but an attempt was made it's also this [8:36] camera a smoothing kind of effects [8:39] you'll notice the camera doesn't [8:41] stick to the player super strictly it'll [8:44] kind of transition between locations [8:46] almost rotate around a bit okay so [8:51] here's the plan I'm going to build a [8:53] clone of that dots sample project and [8:57] I'm going to build it from the ground up [8:58] so I'm not going to use unity or any [9:01] other game engine I'm just going to [9:02] start with a blank codebase and just [9:05] just make the game so to speak and I'm [9:09] going to try to get the gameplay [9:10] experience as similar as possible so I'm [9:12] not just gonna stop with the actual game [9:14] mechanics I'm gonna try get all the you [9:16] know lighting and shadows animations [9:18] sound I'm gonna sure I get try get [9:20] everything there I'm going to use the [9:22] same constraints as the dots one like [9:26] when they were demoing it at there are [9:28] at the conference they mentioned they [9:30] sort of place this restriction on [9:32] themselves not to use object falling for [9:33] the bullets as well I'll stick with that [9:36] constraint also I won't worry about [9:39] being framerate and dependent because [9:41] their version isn't and I'll just do [9:43] discrete collisions I'm going to be [9:46] building it in C++ and keep the [9:49] dependencies to a minimum so I'm going [9:50] to use OpenGL and a window management [9:53] library some libraries for dealing with [9:55] file formats because I don't want to [9:57] have to deal with that stuff and some [9:59] matrix math libraries and after all this [10:02] is done we'll compare the performance of [10:05] these two projects and also try get if [10:07] an estimate of the amount of effort [10:09] required feature these so I am going to [10:13] build this now and I'll see you all soon [10:16] yeah here we are in the hands of clone [10:21] of the sample project it's reasonably [10:26] complete so we got the general gist of [10:28] the game and the guy running around [10:30] Wiggly little monsters chase after you [10:33] shoot a bunch of bullets at them so [10:36] we've also got whatever those subtleties [10:39] were talking about like the shadows I [10:42] think are actually a bit bitter in this [10:44] version and the lighting so you can see [10:48] the the bloom or emission [10:50] effect from its backpack there and that [10:53] other other light areas and the [10:55] animation so you can see he blends [10:58] between all the different angles running [11:02] out there are a couple shortcuts I took [11:06] I'll just gonna running out of time so [11:09] the muzzle flash there that is it's not [11:13] natural particle system like this took [11:15] some some screenshots basically of the [11:18] system using unity and I'm just using a [11:21] spreadsheet to display that also I [11:25] haven't got the camera smoothing we just [11:27] follow him pretty strictly there but I [11:30] don't think those things are going to [11:32] impact the performance measurements too [11:34] much we'll have to make sure we account [11:37] for them when we're doing the effort so [11:43] I was messing around with the hand built [11:44] version of the the sample project [11:46] playing with the numbers a bit and I [11:49] decided to be able to Lee and I said the [11:51] bullets per shot 250 or the sprite [11:55] amount so 2500 bullets per shot but I [11:57] said that the firing rate to zero so [11:59] basically just fire every single frame [12:03] yeah it still performs [12:09] at all yeah so yeah it works pretty well [12:13] we'll do a proper performance analysis [12:16] I'm just mucking around okay so let's [12:19] just clarify how exactly we're going to [12:21] do these comparisons so I'm gonna have [12:24] four enemies read evenly around the [12:27] player the reason I won't have any [12:28] reason there is so that the collision [12:30] detection algorithms can run and the [12:33] performance of those can be included and [12:36] so the enemies will will never die just [12:38] to make sure they're always there and [12:39] every frame and all the tests I'm gonna [12:43] get the character to shoot and the [12:45] consistent direction in all the tests [12:46] and we're going to stick with that fire [12:49] interval of 0.1 seconds in between the [12:52] shots or 100 milliseconds and I'm going [12:55] to gradually increase that spread amount [12:58] variable measuring the the performance [13:01] each time I do that and then I'm going [13:04] to stop once the time to process a [13:06] single frame is equal to that firing [13:09] interval so we're not framerate [13:10] independent at all so we can't actually [13:12] fire bullets faster than frame rate [13:17] allows and so if you curious about the [13:21] hard way that I'm running this on you [13:23] can have a look okay so what are we [13:27] actually going to be measuring so we're [13:29] going to start off with frame time which [13:31] is basically the amount of time elapsed [13:35] in between each frame so it's the [13:38] inverse of frame rate which is a pretty [13:40] common metric to use you might hear the [13:42] term 60 fps or 144 FPS or something like [13:45] that but we're just going to use frame [13:48] time it's a bit bit simpler and it kind [13:51] of matches up better with the other [13:53] metrics we have being consistent with [13:54] lower is better and we're also going to [13:57] look at the resource usage so the usual [14:00] assortment of RAM CPU and GPU and I'm [14:04] just going to use the windows task [14:06] manager for this it's much easier to [14:08] look at the sonnet on a per process [14:10] basis without other applications kind of [14:13] interfering with things and I would like [14:16] to look at startup time but [14:18] unfortunately I don't have the license [14:20] necessary to get rid of the splash [14:21] screen on [14:22] unity release build so I'm just not [14:24] going to not going to look at startup [14:26] time I'm also going to close pretty much [14:28] all other applications while I'm [14:30] performing these tests so I'm not going [14:32] to be recording any of it so I'll see [14:36] you afterwards if you moments later [14:40] okay the results are in and we're gonna [14:44] start off by looking at the frame time [14:46] so this is the amount of time it takes [14:48] us to process every frame in the game [14:51] and again we're going to be averaging [14:53] over a hundred frames and so lower is [14:56] better seven milliseconds is the [14:59] limitation of the hardware that I'm [15:01] using so that corresponds to around [15:03] hundred forty-four frames per second and [15:06] another interesting value to keep in [15:07] mind is 16 milliseconds which [15:10] corresponds to 60 frames per second [15:11] which is a very common set up to have [15:15] and it feels very smooth at 60 frames [15:17] per second hardcore gamers probably [15:19] won't like it but it for most people [15:21] that feels pretty good so data as you [15:26] can see we've got four rows here so the [15:29] first row is the spread amount that was [15:32] the variable we were playing around with [15:34] in the unity editor and I've cloned this [15:37] variable and in the handheld version of [15:39] the project and the second row it's [15:42] simply just the square of that number so [15:44] it's the actual number of bullets we [15:46] fire every shot and here we have the [15:49] frame time of the two different [15:52] implementations this this average value [15:54] over 100 frames and we might go too much [15:58] into this table form a look at a graph [15:59] soon but as you can see when a number of [16:02] entities what bullets is pretty low [16:04] they're both able to to max out the [16:07] frame rate in my in my set up we we hit [16:12] that threshold of 104 milliseconds which [16:15] is which is really long it's not [16:16] playable at all even 72 is probably not [16:19] very playable 27 some people might be ok [16:24] with it a bunch of people will rage quit [16:26] but you can see we hit that limit at [16:28] around 44 thousand bullets per shot and [16:32] the hand-built solution is still [16:34] appearing along pretty nicely there [16:36] at that number of bullets per shot and [16:39] around 420 thousand bullets per shot [16:43] where the hand-built engine kind of [16:46] fails so we look in the graph form we [16:50] can see that difference here so they [16:51] start off very similar and the more [16:53] extreme you go the bigger of differences [16:56] with around a 10x difference up here but [17:00] in these realms that you're probably [17:02] more likely to care about it's more of a [17:03] four to five times the difference so CPU [17:09] utilization this is is not too [17:12] interesting so we just get straight to [17:14] the graph you can see the the hand-built [17:16] version that's pretty steady in the in [17:19] the mid-thirties where's the unity [17:21] version starts off loaded and kind of [17:23] spikes up and has this steep drop and [17:26] we'll see this that steep slope down [17:30] which kind of merit and the GPU [17:33] utilization we're pretty much just falls [17:35] off a cliff and it's it's borderline [17:37] idle when we're at that limit of frame [17:41] time and I think this is a pretty strong [17:45] indication that the unity version is not [17:49] at all bound by GPU or CPU usage I [17:52] suspect that's probably something to do [17:53] with IO between main memory and the GPU [17:58] memory so now we're at the final [18:01] performance metric which is RAM or [18:04] memory usage and I hope you like numbers [18:08] because there you go so again we've got [18:12] these same first two rows of this [18:14] Britomart variable and then the square [18:16] of that number for the actual number of [18:18] bullets per shot and you'll see there's [18:22] a fairly stark difference between the [18:25] RAM used by the unity implementation [18:28] compared with the hand built and it's [18:30] actually a pretty constant ratio between [18:33] the two around 11 or 12 percent of the [18:35] RAM usage for the hand-built one and if [18:40] we take a look on a graph you can see [18:42] that [18:43] yeah the unity one starts a lot higher [18:45] and this slope is much steeper too so [18:49] it's it's around extra 500 bytes of RAM [18:54] used for each bullet we add it doesn't [18:58] necessarily mean all those bytes are for [19:00] the bullet itself could be various [19:02] adjacent and related data structures but [19:04] the application uses 500 bytes more [19:08] memory per bullet and the hampered one [19:11] uses around 40 which makes sense because [19:13] that's exactly what I'm storing in terms [19:17] of where these 500 bytes come from I did [19:19] a little bit of investigation and [19:21] looking at a single entity bullet and [19:26] the entity debugger you can see a [19:28] smorgasbord of data stored here a bit of [19:32] duplication but if we assume some data [19:35] by some data types that's around 200 [19:38] bytes per bullet which still leaves a [19:41] bunch left but I imagine it's something [19:44] to do with it yeah again adjacent data [19:46] structures or maybe storing some KD tree [19:49] of all the entities so you can clip them [19:52] more easily when it comes to render [19:53] things like that [19:54] and the memory per file it didn't [19:56] provide too much insight into this [19:58] either the c-sharp runtime is probably [20:01] also a contributing factor here maybe [20:05] this you know GC references or some [20:09] bunch of data being passed the JIT or [20:11] something like that to summarize all [20:13] these performance comparisons so at the [20:16] frame time of the hand-built version [20:18] range from no difference up to it [20:21] 10x improvements in the most extreme [20:23] conditions when compared to the unity [20:25] version CPU and GPU utilization was [20:29] about equal when the exception [20:30] applications were running smoothly and [20:33] memory usage of the unity version was [20:37] about nine times that of the handheld [20:40] version right so time to look at the [20:43] other side of the equation how much more [20:45] work is it to build this application [20:48] from the ground up instead of using a [20:50] game engine unity in this case so we [20:54] want to get a ratio you know is it twice [20:56] as much if it [20:57] times as much if it something like that [21:00] and so we're going to use this little [21:02] formula here we got Aoife if it and the [21:05] little subscripts HB is for hand-built [21:08] and subscript unity is for the unity [21:11] version and so we take the ratio of [21:13] these two or you can see how much more [21:15] if it it is and I didn't make the unity [21:18] version so some assumptions will be [21:21] needed but we'll we'll do the best we [21:23] can and so to calculate the fat ratio [21:26] we'll need a unit of measurement and [21:29] we'll need to use the same unit for for [21:33] both of them [21:34] doesn't really matter too much what it [21:35] is but we just have to make sure it's [21:36] the same so for convenience I'm going to [21:40] use my unit of measurement as the effort [21:43] required for the average line of c-sharp [21:45] code in the unity project okay so these [21:50] are the phone as I came up with for [21:53] unity we start out with the number of [21:55] c-sharp lines in the unity project [21:58] however not all of the behavior of the [22:02] unity project is defined in the c-sharp [22:05] code there's a lot of stuff that gets [22:08] defined outside this you know dealing [22:09] with materials and animation graphs and [22:12] lighting and scene layout all that stuff [22:14] and that's not captured here so that's [22:17] why we got this second little factor [22:19] here in brackets which is basically how [22:22] much of the the total effort is relative [22:26] to the the coding of it so it's twice as [22:29] much if it to get the whole thing done [22:30] then this will be a factor of two and [22:34] for the hand built we started out with [22:37] the number of lions in the handle [22:39] project now the if it required for one [22:43] of these lines is not necessarily equal [22:45] to the average line of C shop code in [22:48] the unity project so we got this little [22:50] effector here to try and bring those [22:53] into line and this last one here is just [22:56] to account for the fact that there are a [22:58] couple little things missing and the the [23:00] handbill version so we we need to [23:02] account for that so for unity this a [23:06] reminder there's our equation let's try [23:09] and find the values to [23:11] variables so the number of lines in the [23:14] unity project was 542 so this is if your [23:18] ignore or the blank lines and comments [23:20] and I removed all the non easiest Co [23:23] paths and I changed the code style to [23:28] match that of the hand-built so I just [23:29] put all the curly braces up things like [23:32] that and this other factor of the the [23:36] total effort relative to the coding if [23:38] it I put that as 2 so there are there [23:43] are a lot of elements to defining your [23:46] unity project that are outside the code [23:47] and from my personal experience I've [23:51] never worked on large-scale unity [23:53] projects just a little small toy [23:55] projects the thing we're looking at [23:58] happens to be a small toy project so [23:59] that probably Maps pretty well and I did [24:01] find that you know generally about half [24:03] the time was in the code and the other [24:05] half was in the the non code stuff so [24:08] that brings our unity if it value up to [24:12] 1080 for now for the hand-built version [24:16] again this was our formula and the [24:19] number of lines in the project was two [24:22] thousand six hundred and nineteen and [24:24] similarly get rid of all that stuff and [24:27] this includes everything so the C++ GLSL [24:31] which is the the shader language for [24:33] OpenGL for writing code on the GPU that [24:36] runs on the GPU and the build [24:38] configuration as well so everything is [24:41] in there the line if it ratio now [24:45] originally I was going to have a much [24:46] higher number for this something like [24:48] around two because this you know this [24:51] and some complicated math you end up [24:53] having to do when you're kind of you [24:55] know doing the ray tracing yourself or [24:57] when you're trying to figure out where [24:59] the guy is aiming based on your mouse [25:00] coordinates things like that but then I [25:05] looked at the unity code and the easiest [25:07] stuff is actually not it's non-trivial [25:09] and a lot of the complicated math that [25:12] you end up doing the hand-built version [25:13] let's spread over many lines anyway so [25:15] it's picked up that pretty well the [25:18] reason I did give it a bit of a boost [25:21] though is because debugging OpenGL and [25:24] the C++ code related to the Earthmen GL [25:27] angeas GLSL is pretty troublesome to to [25:30] debug I've often found myself just [25:33] rendering pixels of particular colors to [25:35] try and figure out what the state was in [25:37] the GLSL code which it does slow down [25:40] development a bit and completion it was [25:44] most there but there was a few things [25:46] missing so I gave myself 90% completion [25:49] so we end up with an effort value of [25:53] three thousand six hundred and thirty [25:56] eight all right so let's put it all [25:59] together so the relative effort of the [26:03] hand-built this is the unity with these [26:06] numbers it works out to be three point [26:08] four so get rid of that some of that [26:11] false precision and we're left with [26:12] three to four times the effort of [26:15] building the handle version of this [26:17] particular game compared with the unity [26:20] version and achieve Li that that seems [26:23] about right yeah so like a skilled unity [26:25] developer could probably get the project [26:27] done in a day and someone building it by [26:32] hand if they're sort of familiar with [26:34] all the math and i comfortab with that [26:35] and a comfortable c++ they could [26:37] probably get this working in a week with [26:39] a bit of time to spare in conclusion the [26:43] handbells solution for this game [26:46] provides strictly better performance for [26:48] the measurements that we did with a very [26:51] significant difference for the memory [26:53] usage and potentially significant [26:57] difference for the the framerate or [26:58] frame time the difference and amount of [27:02] work required was also very significant [27:04] and most of the time building the [27:07] hand-built version was spent on those [27:09] polished features getting there lighting [27:11] and shadows and bloom and animation [27:13] blending getting all that stuff working [27:15] and just right that that's where mostly [27:18] if it went getting the actual game [27:19] mechanics working and getting it all [27:21] optimized that was it was actually [27:23] pretty straightforward and I think it's [27:26] important just to give out a reminder [27:27] that you know this is not a [27:29] one-size-fits-all comparison for [27:32] different projects different teams all [27:34] these different variables are going to [27:35] change this hugely [27:38] so I'm just just giving a single data [27:40] point here and so be wary of [27:43] extrapolating from that too much all [27:46] right so that's it thank you for [27:48] watching I will be keeping an eye on the [27:50] the comments section of this video so if [27:52] you have any thoughts or questions feel [27:54] free to post them