The Code Generator Journey: From Manual Hell to Declarative Heaven
How we built a YAML-to-C++ code generator in PgScript, eliminated 559+ lines of boilerplate, and made ECS components 6x faster to create
The Problem: Writing the Same Code for the 50th Time
Picture this: You’re building a game engine with a pure Entity Component System (ECS) architecture. You need to add a new component—let’s call it HealthComponent. Simple enough, right? You need a few fields: currentHealth, maxHealth, and isDead.
But wait. In practice, here’s what you actually need to write:
- Component struct definition - Fields, getters, setters
- Event-firing setters - Notify systems when values change
- VM proxy schema - Expose component to scripting system
- Serialization code - Save/load support
- Attach handlers - Let scripts create and attach components
For our “simple” HealthComponent, that’s easily 100+ lines of boilerplate code. And you’ll write it all by hand. Again. For the 50th time.
The Real Cost: PositionComponent
Let me show you what “boilerplate” really means with a real example from ColumbaEngine. Our PositionComponent handles 2D position, size, rotation, and visibility. It has 8 fields. Here’s what we had to maintain:
- position.h: 359 lines - Struct definition, enums, helper structs
- position.cpp: 150 lines - Setter implementations, utility functions
- ecsserialization.cpp: 98 lines - VM proxy and serialization
Total: ~607 lines of code for ONE component with 8 fields.
Every time we wanted to add a field:
- Update the struct definition
- Write getter/setter with event firing
- Add to VM proxy schema
- Update serialization code
- Register with attach handler
- Hope we didn’t forget anything
The Maintenance Nightmare
Copy-paste errors were common. Event firing was inconsistent. Adding viewport to TTFText? I forgot to update the serialization. Position’s rotation setter wasn’t firing events properly. The serialization for colors used a different pattern than other fields.
Every component felt like writing the same code with slightly different names.
And that’s when I asked: What if we didn’t have to?
The Vision: Declarative Components
What if defining a component looked like this instead:
name: PositionComponent
namespace: pg
base_class: Component
fields:
- name: x
type: float
default: 0.0f
setter: event
- name: y
type: float
default: 0.0f
setter: event
- name: rotation
type: float
default: 0.0f
setter: event
That’s it. Just declare what you want. The code writes itself.
Benefits:
- Single source of truth - One
.pgcompfile defines everything - Consistency guaranteed - Generated code follows the same patterns
- Easy to read and modify - YAML is human-friendly
- Regenerate anytime - Change something? Regenerate. No fear.
Why Build Our Own Generator?
Before diving in, we considered alternatives:
Option 1: External Templating (Jinja2, Mustache, etc.)
Pros: Mature, well-tested Cons: Python dependency, another language to learn, separate tooling
Option 2: Python Generator Scripts
Pros: Flexible, easy to write Cons: External dependency, not integrated with engine
Option 3: C++ Template Metaprogramming
Pros: No runtime overhead Cons: Compile-time cost, hard to read, limited flexibility
Our Choice: PgScript
We chose to write the generator in PgScript, ColumbaEngine’s own scripting language.
Why?
- Dogfooding - Use the engine’s own language to build engine tools
- Zero external dependencies - Ships with the engine
- Demonstrates capability - Shows PgScript is production-ready
- Faster iteration - No recompilation needed
- Educational - Others can read and learn from it
The decision to use PgScript forced us to make the language better. And that benefited everyone.
Building the Foundation: Native Modules
Before we could write a component generator in PgScript, we needed to enhance the language itself.
String Module: 349 Lines of Utilities
A YAML parser needs string manipulation. Lots of it. So we added:
// String inspection
startsWith(str, prefix) // "PositionComponent" starts with "Position"
endsWith(str, suffix) // "component.h" ends with ".h"
contains(str, substring) // "type: float" contains "float"
// String manipulation
substring(str, start, end) // Extract portions
trim(str) // Remove whitespace
replace(str, from, to) // Replace all occurrences
// String transformation
capitalize(str) // "position" -> "Position"
indexOf(str, substring) // Find position of substring
Commit: 191babed - Added 349 lines to stringmodule.h
File Module: Reading and Writing
import "file"
var content = readFile("PositionComponent.pgcomp")
var success = writeFile("PositionComponent.generated.h", generatedCode)
Simple but essential. The generator reads .pgcomp files and writes generated .h and .cpp files.
Algorithm Module: Vector Operations
YAML has arrays. Arrays everywhere. Field lists, include lists, method lists. We needed robust array manipulation to build the generator.
What we added to the algorithm module:
import "algorithm"
// Array operations
var fields = []
push(fields, fieldDef) // Add to end
insert(fields, 0, fieldDef) // Insert at index
var field = pop(fields) // Remove and return last
removeAt(fields, 2) // Remove at specific index
// Length and access
var count = len(fields) // Get array length
var field = fields[0] // Access by index
// Type checking and introspection
var type = typeOf(fields) // Returns "array"
if (typeOf(value) == "string") // Dynamic type checking
// Table/object operations
if (contain(currentItem, "name")) // Check if key exists
{
var name = currentItem["name"]
}
// Type conversion
var number = toInt("42") // String to integer
Why these functions matter:
push()andpop()- Essential for building arrays during parsinglen()- Needed for loops and bounds checkingcontain()- Critical for checking if YAML fields exist before accessingtypeOf()- Dynamic type checking for flexible parsingtoInt()- Converting string numbers in YAML to integers
Without these utilities, we’d have no way to work with the structured data coming from YAML. Arrays are first-class citizens in the component generator.
Commit: 4db26392 - Added 158 lines of vector manipulation
Commit: ebc08c06 - Added typeOf() utility
With these modules in place, PgScript had everything needed for the generator: string manipulation, file I/O, and data structure operations.
The YAML Parser Challenge
Why YAML?
We needed a format for .pgcomp files that was:
- Human-readable - Developers edit these by hand
- Structured - Support nested objects and arrays
- Standard - Don’t invent a new format
- Comment-friendly - Documentation inline
YAML checked all the boxes. But… we had to write a YAML parser. In PgScript.
Parser Architecture
Here’s the core structure from component_generator.pg:
class YamlParser
{
init()
{
this.indentStack = []
}
parse(content)
{
var lines = splitLines(content)
var result = {}
var currentSection = ""
var currentArray = []
var currentItem = {}
for (var i = 0; i < len(lines); i++)
{
var line = lines[i]
var trimmed = trim(line)
// Skip empty lines and comments
if (len(trimmed) > 0 and not startsWith(trimmed, "#"))
{
var indent = this.getIndent(line)
if (startsWith(trimmed, "-"))
{
// Array item
this.parseArrayItem(trimmed, currentItem, currentArray)
}
else if (contains(trimmed, ":"))
{
// Key-value pair
this.parseKeyValue(trimmed, indent, result, currentSection)
}
}
}
return result
}
}
The Tricky Parts
Writing a YAML parser from scratch revealed complexity we didn’t anticipate. Here are the challenges we had to solve:
1. Indentation Tracking
YAML uses indentation for nesting. We needed to count spaces to determine structure:
getIndent(line)
{
var indent = 0
for (var i = 0; i < len(line); i++)
{
var char = substring(line, i, i + 1)
if (char == " ")
indent = indent + 1
else
break
}
return indent
}
The challenge: Different indent levels mean different things:
fields: # indent = 0 (top-level)
- name: x # indent = 2 (array item)
type: float # indent = 4 (nested property)
default: 0.0f # indent = 4 (same level)
We maintain a state machine tracking current section, current array, and current object. When indent decreases, we know we’re back to a parent level.
2. Array Items with Key-Value Pairs
This YAML pattern was particularly tricky:
fields:
- name: x # Array item with nested key-value
type: float
default: 0.0f
- name: y # Next array item
type: float
The problem: The - starts a new array item, but the following indented lines are properties of that item.
Our solution:
var currentItem = {}
if (startsWith(trimmed, "-"))
{
// Save previous item if it exists
if (contain(currentItem, "name") or contain(currentItem, "params"))
{
push(currentArray, currentItem)
}
// Start new item
currentItem = {}
// Parse the line after the dash
var afterDash = trim(substring(trimmed, 1, len(trimmed)))
if (contains(afterDash, ":"))
{
// "- name: x" format
var parts = this.splitFirst(afterDash, ":")
var key = trim(parts[0])
var value = trim(parts[1])
currentItem[key] = this.parseValue(value)
}
}
else if (indent > 0 and contains(trimmed, ":"))
{
// Nested property of current item
var parts = this.splitFirst(trimmed, ":")
var key = trim(parts[0])
var value = trim(parts[1])
currentItem[key] = this.parseValue(value)
}
We accumulate properties into currentItem, then push it to the array when we see the next - or reach a different section.
3. Type Inference for Values
YAML values can be strings, numbers, booleans, arrays, or objects. We needed to parse them correctly:
parseValue(value)
{
// Parse inline arrays: [item1, item2, item3]
if (startsWith(value, "[") and endsWith(value, "]"))
{
var innerContent = substring(value, 1, len(value) - 1)
var items = split(innerContent, ",")
var result = []
for (var i = 0; i < len(items); i = i + 1)
{
push(result, trim(items[i]))
}
return result
}
// Parse inline objects: {key1: value1, key2: value2}
if (startsWith(value, "{") and endsWith(value, "}"))
{
// ... parse object
return obj
}
// Parse booleans
if (value == "true")
return true
if (value == "false")
return false
// Parse numbers (kept as strings for simplicity)
// Everything else is a string
return value
}
For simplicity, we keep most values as strings and let the code generator handle type conversion. This avoided complex number parsing.
4. Comment Handling
Comments (#) should be ignored:
fields:
- name: x # X coordinate
type: float # Using float for precision
Simple solution:
// Skip empty lines and comments
if (len(trimmed) > 0 and not startsWith(trimmed, "#"))
{
// Process line
}
We check this early and skip commented lines entirely.
Parser Statistics
The complete YAML parser is:
- ~400 lines of PgScript
- Supports:
- Top-level key-value pairs
- Nested sections
- Arrays with
-notation - Objects within arrays
- Inline arrays
[a, b, c] - Inline objects
{key: value} - Comments
- Does NOT support:
- Multiline strings
- Anchors and aliases (
&,*) - Complex multiline syntax (
|,>) - Explicit typing (
!!str,!!int) - Merge keys (
<<:)
Why this is enough: We control the .pgcomp format. We don’t need full YAML spec compliance—just enough to make component definitions readable and writable by humans.
The parser works perfectly for our needs, and we can extend it when new features require new YAML constructs.
Code Generation: The Heart of the System
Now for the interesting part: generating C++ code from YAML definitions.
The .pgcomp Format
Here’s a complete example - our Health component:
name: HealthComponent
namespace: pg
includes:
- <cstdint>
fields:
- name: currentHealth
type: int32_t
default: 100
setter: event
- name: maxHealth
type: int32_t
default: 100
setter: basic
- name: isDead
type: bool
default: false
setter: none
Simple, readable, and complete. Let’s see what gets generated.
Header Generation
From the YAML above, we generate:
// PositionComponent.generated.h
// AUTO-GENERATED - DO NOT EDIT
#pragma once
#include <cstdint>
#include "ECS/system.h"
namespace pg
{
struct HealthComponent : public Component
{
// Fields
int32_t currentHealth = 100;
int32_t maxHealth = 100;
bool isDead = false;
// Setters
void setCurrentHealth(int32_t value); // event setter
void setMaxHealth(int32_t value); // basic setter
// No setter for isDead (setter: none)
// ECS integration
_unique_id id = 0;
EntitySystem* ecsRef = nullptr;
};
}
Clean, consistent, and complete.
Setter Levels
This is where the system shines. We support 4 levels of setters, each for different needs:
Level 1: No Setter (Direct Access)
- name: isDead
type: bool
default: false
setter: none
Generated: Just the field. Scripts access it directly.
Use case: Derived state, internal flags
Level 2: Basic Setter
- name: maxHealth
type: int32_t
default: 100
setter: basic
Generated:
void setMaxHealth(int32_t value) {
maxHealth = value;
}
Use case: Simple assignment with validation opportunity
Level 3: Event Setter (The Key Innovation)
- name: currentHealth
type: int32_t
default: 100
setter: event
Generated:
void setCurrentHealth(int32_t value)
{
if (this->currentHealth != value) // Only fire if changed!
{
this->currentHealth = value;
if (ecsRef)
{
ecsRef->sendEvent(HealthComponentChangedEvent{id});
}
}
}
The optimization: if (this->currentHealth != value)
This one line prevents cascading updates. Before code generation, we wrote this check manually (sometimes). Now it’s guaranteed for every event setter.
Before the generator, we had code like this in PositionComponent:
void PositionComponent::setX(float x)
{
if (areNotAlmostEqual(this->x, x)) // Sometimes we remembered the check
{
LOG_THIS(DOM);
this->x = x;
if (ecsRef)
ecsRef->sendEvent(PositionComponentChangedEvent{id});
}
}
But other setters forgot the check. Inconsistent. Now it’s automatic.
Use case: Components that trigger system updates, fields that affect rendering or game state
Future: Level 4 - Custom Setters
We’re planning a 4th level for complex custom logic:
- name: wrap
type: bool
default: false
setter: custom
custom_code: |
this->wrap = value;
this->changed = true; // Custom logic
recalculateLayout(); // More custom logic
fireEvent();
This will allow embedding arbitrary C++ code for setters that need more than just event firing. Not yet implemented, but on the roadmap!
Constructor Generation
Components often need constructors with default parameters:
constructors:
- params: [text, fontPath, scale, colors]
defaults: {colors: "{255.0f, 255.0f, 255.0f, 255.0f}"}
Generated:
TTFText(const std::string& text,
const std::string& fontPath,
float scale,
constant::Vector4D colors = {255.0f, 255.0f, 255.0f, 255.0f})
: text(text)
, fontPath(fontPath)
, scale(scale)
, colors(colors)
{}
Member initialization, default parameters, proper formatting—all automatic.
The Bootstrap Problem
Now we hit an interesting challenge: How do you build an engine that needs components when the components are generated by the engine?
This is a classic chicken-and-egg problem:
- We need the engine to run PgScript
- PgScript runs the component generator
- The component generator creates components
- The engine needs those components to build
Solution: The Bootstrap Compiler
Commit: b7508293 - “Can now build a minimal pgcompiler without any graphical deps”
We created a minimal PgCompiler that:
- Has zero graphical dependencies (no SDL, no OpenGL)
- Can run PgScript programs
- Compiles separately from the main engine
- Runs during CMake configuration phase
CMake Integration
# Build minimal bootstrap compiler first
add_executable(pgcompiler_bootstrap
src/Engine/Compiler/*.cpp
# Exclude graphical systems
)
# Generate components before main build
file(GLOB PGCOMP_FILES "src/Engine/Components/*.pgcomp")
foreach(PGCOMP_FILE ${PGCOMP_FILES})
get_filename_component(COMP_NAME ${PGCOMP_FILE} NAME_WE)
add_custom_command(
OUTPUT "${CMAKE_BINARY_DIR}/Generated/${COMP_NAME}.generated.h"
COMMAND pgcompiler_bootstrap
tools/component_generator.pg
${PGCOMP_FILE}
DEPENDS ${PGCOMP_FILE} tools/component_generator.pg
COMMENT "Generating ${COMP_NAME} component..."
)
endforeach()
Build flow:
- CMake configures
- Bootstrap compiler builds (fast, no graphics deps)
- Component generator runs for each
.pgcompfile - Generated files created in
build/Generated/ - Main engine build includes generated files
Beautiful.
Preventing Rebuild Loops
Problem: The generator ran every time, triggering full rebuilds even when nothing changed.
Commit: 3f771c7d - “Only regenerate .pgcomp file on change”
Solution:
add_custom_command(
OUTPUT "${GENERATED_FILE}"
COMMAND pgcompiler_bootstrap ... > "${GENERATED_FILE}.tmp"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${GENERATED_FILE}.tmp"
"${GENERATED_FILE}"
DEPENDS ${PGCOMP_FILE}
)
copy_if_different only updates the file if content actually changed. This preserves timestamps and prevents unnecessary recompilation.
Migration: Real Impact with Real Numbers
Time to migrate our existing components and measure the impact.
PositionComponent - The Big One
PositionComponent is complex. It handles 2D transforms, anchoring, visibility, events, and more.
Before: The Manual Implementation
position.h (359 lines):
#pragma once
#include "ECS/system.h"
#include "pgconstant.h"
namespace pg
{
enum class AnchorType : uint8_t { ... };
enum class ResizeHandle : uint8_t { ... };
struct PositionComponentChangedEvent
{
_unique_id id = 0;
};
struct PositionComponent : public Component
{
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
float width = 0.0f;
float height = 0.0f;
float rotation = 0.0f;
bool visible = true;
bool observable = true;
// Getters
float getX() const { return x; }
float getY() const { return y; }
// ... 6 more getters
// Setters with event firing
void setX(float x);
void setY(float y);
// ... 6 more setters
// Utility methods
bool isVisible() const { return visible; }
bool isObservable() const { return observable; }
bool isRenderable() const { return visible && observable; }
// ... more methods
_unique_id id = 0;
EntitySystem* ecsRef = nullptr;
};
}
position.cpp (150 lines):
void PositionComponent::setX(float x)
{
if (areNotAlmostEqual(this->x, x))
{
LOG_THIS(DOM);
this->x = x;
if (ecsRef)
ecsRef->sendEvent(PositionComponentChangedEvent{id});
}
}
// Repeat for setY, setZ, setWidth, setHeight, setRotation...
ecsserialization.cpp (98 lines for this component):
// VM proxy schema
auto positionProxySchema = [](VM* vm, EntityRef entity) -> Value {
auto component = entity.getComponent<PositionComponent>();
// ... 100 lines of proxy setup
};
// Serialization
archive("x", component->x);
archive("y", component->y);
// ... serialization for each field
Total: ~607 lines
After: The Declarative Implementation
PositionComponent.pgcomp (62 lines):
name: PositionComponent
namespace: pg
base_class: Component
includes:
- "ECS/system.h"
- "pgconstant.h"
forwards:
- "struct UiAnchor;"
fields:
- name: x
type: float
default: 0.0f
setter: event
- name: y
type: float
default: 0.0f
setter: event
- name: z
type: float
default: 0.0f
setter: event
- name: width
type: float
default: 0.0f
setter: event
- name: height
type: float
default: 0.0f
setter: event
- name: rotation
type: float
default: 0.0f
setter: event
- name: visible
type: bool
default: true
setter: event
- name: observable
type: bool
default: true
setter: event
methods:
- "bool isVisible() const { return visible; }"
- "bool isObservable() const { return observable; }"
- "bool isRenderable() const { return visible and observable; }"
- "bool updatefromAnchor(const UiAnchor& anchor);"
- "void setVisibility(const bool& value);"
events:
- PositionComponentChangedEvent
Total: 62 lines
The Numbers
Reduction: 607 lines → 62 lines = 90% smaller
The Impact: Beyond Line Counts
Development Velocity
Adding a new component:
-
Before:
- Write struct definition
- Implement setters with event firing
- Add VM proxy schema
- Write serialization code
- Register attach handler
- Debug inconsistencies
-
After:
- Write
.pgcompdefinition - Run generator
- Done
- Write
Adding a field to existing component:
-
Before:
- Update struct
- Add getter/setter
- Update proxy
- Update serialization
- Pray you didn’t forget anything
-
After:
- Add 4 lines to
.pgcomp - Regenerate
- Add 4 lines to
Faster iteration, and less bug prone
Bug Reduction: The “Only Fire When Changed” Story
During code generation implementation, we discovered an important optimization:
void setX(float value)
{
if (this->x != value) // This check!
{
this->x = value;
if (ecsRef)
{
ecsRef->sendEvent(PositionComponentChangedEvent{id});
}
}
}
The problem: Without the if (this->x != value) check, setting a field to its current value fires an event. Systems respond to that event. Those systems might set more fields. More events fire. Cascading updates.
Before the generator: This check was manual. Sometimes we remembered. Sometimes we forgot. Inconsistent.
After the generator: This check is in every event setter. Guaranteed. Consistent.
Commit: 25754476 - “Fixed event generation in component to only fire when the value changes”
One fix in the generator → all components benefit forever.
Documentation Benefits
The .pgcomp files double as documentation:
Before: “What fields does PositionComponent have?” → Open position.h, scroll through 359 lines of code
After: “What fields does PositionComponent have?” → Open PositionComponent.pgcomp, see 8 clear field definitions
fields:
- name: x
type: float
default: 0.0f
setter: event
Type, default value, behavior—all visible at a glance.
New team members understand components in minutes, not hours.
Lessons Learned: What Worked and What Didn’t
What Went Exceptionally Well
Dogfooding Accelerated Language Development
Using PgScript to build engine tools revealed gaps:
- String manipulation was basic → Added 46+ utility functions
- File I/O needed write support → Added
writeFile() - Array operations needed helpers → Added vector module
- Type introspection was missing → Added
typeof()
Every gap we filled made PgScript more capable. Not just for the generator, but for all PgScript users.
Lesson: Use your own tools. The pain you feel is real user pain.
Incremental Approach Reduced Risk
We didn’t migrate everything at once:
- Phase 1: Write the generator (simple Health component example)
- Phase 2: Migrate PositionComponent (medium complexity)
- Phase 3: Migrate TTFText (complex, with custom constructors)
- Phase 4: Generate serialization and VM proxies
Each phase validated the approach. Each success built confidence.
Lesson: Small iterations makes it easier for refactoring old or complex systems.
What Was Challenging
YAML Parsing Complexity
Writing a YAML parser is harder than it looks:
- Indentation tracking - Spaces vs tabs, mixed indentation
- Nested structures - Objects within arrays within objects
- Edge cases - Empty values, comments, multiline strings
- Error handling - Where exactly did parsing fail?
The parser is now 400+ lines and still doesn’t handle all YAML edge cases. We support the subset we need, but adding new .pgcomp features sometimes means parser changes.
Lesson: Consider an existing parser library if your language supports it. We couldn’t, but you might be able to.
Build System Integration Took Iteration
The bootstrap compiler solution works, but we went through multiple iterations:
- Attempt 1: Generate during build -> rebuild loops
- Attempt 2: Generate in separate step -> dependency issues
- Attempt 3: Bootstrap compiler -> chicken-and-egg problem
- Final solution: Minimal bootstrap +
copy_if_different
CMake’s dependency tracking is subtle. Getting it right required patience and testing.
Lesson: Build system integration is never as simple as you think. Plan extra time.
Balancing Flexibility vs Simplicity
We have 4 setter levels. Is that too many? Not enough?
- Some components need custom logic
- Most components need simple patterns
- Adding flexibility adds complexity
We’re still finding the right balance. Too many options overwhelm users. Too few options make the system inflexible.
Lesson: Start simple. Add flexibility only when needed. Resist feature creep.
Future Improvements
Ideas for next iteration:
- Better error messages - Parser errors should show line numbers
- Validation - Catch typos in setter levels, invalid types
- Component relationships - Parent/child components, dependencies
- IDE integration - Syntax highlighting for
.pgcompfiles - Hot-reload - Change
.pgcomp, see changes in editor without rebuild - Type checking - Validate field types against C++ types
- Automatic Docs Creation - Reuse the YAML parser to automatically create the docs of the components in our official docs
Conclusion: The Power of Meta-Programming
Let’s return to where we started: the 50th component.
Before the generator: Sigh. Another component. Open position.h, copy-paste struct definition, update field names, write 8 setters, add proxy schema, write serialization, register attach handler, test, debug, commit. 45 minutes later…
After the generator:
Create WeaponComponent.pgcomp, define 5 fields, run generator. Done. 5 minutes.
That’s the power of code generation. But the benefits run deeper:
Key Takeaways
1. Boilerplate is a Signal
Repetitive code isn’t just annoying—it’s a signal. If you’re writing the same pattern over and over, automate it.
2. Dogfooding Accelerates Development
Using PgScript to build PgScript tools made the language better. Use your own creations. Feel the pain. Fix it.
3. Declarative > Imperative
Compare these:
- 607 lines of imperative C++
- 62 lines of declarative YAML
Which would you rather maintain?
4. Code Generation Compounds
The first component saved 545 lines. The second saved 200 more. The third will save even more. Benefits multiply with scale.
5. Meta-Programming is Accessible
You don’t need complex C++ template magic. A simple script can generate code. YAML + string concatenation is often enough.
What’s Your Boilerplate?
Think about your codebase:
- What do you copy-paste frequently?
- What patterns do you repeat?
- Where do inconsistencies creep in?
That’s your opportunity for code generation.
Maybe it’s:
- Database models
- API endpoints
- Serialization code
- Unit test boilerplate
- UI component definitions
Whatever it is, consider: Could a generator help?
Final Thought
“The best code is the code you don’t have to write.”
The component generator doesn’t just save time—it eliminates entire classes of bugs, enforces consistency, and makes our ECS architecture more accessible to everyone on the team.
559 lines eliminated. Countless bugs prevented. Infinite time saved.
That’s the journey from manual hell to declarative heaven.
Resources
- ColumbaEngine Repository: github.com/Gallasko/ColumbaEngine
- Component Generator Source: tools/component_generator.pg
- Documentation:
Key Commits Referenced
- 9f061bb6 - Start working on generator tool
- 191babed - Added native functions for string/file manipulation
- 3883d054 - Working basic component generator
- 4c6f4b5a - Added bootstrap compiler
- b7508293 - Minimal pgcompiler without graphical deps
- 3f2ecc26 - Position component migration (-208 lines)
- a9f154a4 - TTFText migration with constructor generation
- 25754476 - Fixed event generation optimization
- 3f771c7d - Only regenerate on change
Want to discuss code generation, ECS architecture, or game engine development? Find me on Discord or check out columbaengine.org.