TA logoThe Technical Artist
Home/Indie Dev/Game Engines
article 03
indie dev

Choosing A Game Engine

Workingguide~9 min readreviewed 2026-07-05

Unity, Unreal, Godot, and the open-source crowd, compared through the stuff TAs actually feel day to day: scripting, shaders, pipeline hooks, iteration speed, and licensing.

Choosing an engine is one of the big indie calls. As a TA, you're not only asking "can it render the game?" You're asking how painful imports are, whether tools are scriptable, how shaders get authored, and how fast the team can iterate without fighting the editor.

Tip

There isn't a universal best engine. There's only the engine that fits this project, this team, and this budget. Prototype in your top two choices before you marry one.

3.1Unity

Unity is where a lot of indie games live, and it earns that by being decent at almost everything: 2D, mobile, stylized 3D, tools, weird prototypes, the lot. For a TA, the draw is simple: good editor scripting, a huge ecosystem, and answers to almost any question one search away.

Strengths

  • Huge ecosystem - Asset Store tools, art packs, systems, tutorials. If you need it, someone has probably tried to sell or share it.
  • C# scripting - Clean enough to learn, deep enough for real editor tools, custom inspectors, and windows.
  • Shader Graph - Node shaders that cover most indie needs without making every artist learn HLSL.
  • Cross-platform builds - PC, console, mobile, and web from one project, with a well-worn build path.
  • SRP - URP and HDRP give TAs real rendering control. Custom SRP is there if you want the deep end.

Weaknesses

  • Pricing changes - Unity's runtime fee controversy damaged trust. Check current licensing terms carefully before committing.
  • Performance ceiling - Unity can achieve good performance, but it needs more manual optimization than Unreal for visually complex scenes.
  • Inconsistent documentation - Docs vary in quality. Some newer features are poorly documented or have outdated examples.

TA Workflow: Custom Editor Tool In Unity

This is the kind of tiny Unity editor tool TAs write constantly: select objects, type a pattern, rename the lot with undo support.

C#
using UnityEditor;
using UnityEngine;

public class BatchRenamer : EditorWindow
{
 private string prefix = "";
 private string baseName = "Object";
 private int startIndex = 1;

 [MenuItem("Tools/TA/Batch Renamer")]
 public static void ShowWindow()
 {
 GetWindow<BatchRenamer>("Batch Renamer");
 }

 private void OnGUI()
 {
 GUILayout.Label("Batch Rename Selected Objects", EditorStyles.boldLabel);
 prefix = EditorGUILayout.TextField("Prefix", prefix);
 baseName = EditorGUILayout.TextField("Base Name", baseName);
 startIndex = EditorGUILayout.IntField("Start Index", startIndex);

 if (GUILayout.Button("Rename Selected"))
 {
 GameObject[] selected = Selection.gameObjects;
 Undo.RecordObjects(selected, "Batch Rename");

 for (int i = 0; i < selected.Length; i++)
 {
 selected[i].name = $"{prefix}{baseName}_{startIndex + i:D3}";
 }
 }
 }
}
Note

Unity editor scripts live in an Editor folder and use the UnityEditor namespace. They're stripped from builds automatically, so they don't affect your game's runtime performance.

3.2Unreal Engine

Unreal is the heavy-duty choice. If you need high-end visuals, open worlds, cinematic lighting, or an editor built around big content teams, it's hard to ignore. For TAs, the big wins are the material editor, Blueprints, Python, and the fact that the engine expects serious content pipelines.

Strengths

  • Visual ceiling - Nanite, Lumen, virtual shadow maps, and MetaHumans make beautiful results less heroic than they used to be.
  • Material Editor - Probably the strongest node material system in games. You can go very far before writing HLSL.
  • Blueprints - Great for artist-facing actors, prototypes, editor utilities, and gameplay glue.
  • Source access - If you truly need engine changes, the code is there. You still have to survive reading it.
  • Niagara - Deep VFX system with GPU simulation, mesh particles, and custom data interfaces.

Weaknesses

  • Steep ramp - Unreal is big. Expect more setup, more concepts, and more "why is this asset still referencing that thing?" moments.
  • C++ when BP runs out - Blueprints go far, but eventually serious runtime systems want C++.
  • Build and shader compile time - Modest hardware feels the pain quickly.
  • Royalty model - 5% royalty on gross revenue above $1M per product per year. Free below that threshold.

TA Workflow: Dynamic Material Instance In Unreal

This snippet drives global material values at runtime. Weather, wetness, damage states, time of day - this is the pattern behind a lot of game-wide visual control.

C++
// Header: DynamicWeatherController.h
UCLASS()
class ADynamicWeatherController : public AActor
{
 GENERATED_BODY()

public:
 UPROPERTY(EditAnywhere, Category = "Weather")
 UMaterialParameterCollection* WeatherParams;

 UPROPERTY(EditAnywhere, Category = "Weather", meta = (ClampMin = 0, ClampMax = 1))
 float RainIntensity = 0.0f;

 UFUNCTION(BlueprintCallable, Category = "Weather")
 void UpdateWeather(float DeltaTime);
};

// Source: DynamicWeatherController.cpp
void ADynamicWeatherController::UpdateWeather(float DeltaTime)
{
 if (!WeatherParams) return;

 UWorld* World = GetWorld();

 // Update global material parameters that all shaders can read
 UKismetMaterialLibrary::SetScalarParameterValue(
 World, WeatherParams, "RainIntensity", RainIntensity);

 UKismetMaterialLibrary::SetScalarParameterValue(
 World, WeatherParams, "WetnessAmount",
 FMath::Lerp(0.0f, 1.0f, RainIntensity));

 UKismetMaterialLibrary::SetVectorParameterValue(
 World, WeatherParams, "RainDirection",
 FLinearColor(0.1f, -1.0f, 0.2f, 0.0f));
}
Tip

Material Parameter Collections are one of Unreal's best TA levers. Define the value once, read it in any material, and suddenly weather, time of day, and global style controls stop being copy-pasted material parameters.

3.3Godot

Godot is the lightweight, open-source option that keeps getting harder to dismiss. It's not the best answer for every 3D game, but for 2D, stylized 3D, jams, small teams, and anyone tired of licensing drama, it feels refreshingly direct.

Strengths

  • Actually free - MIT license, no royalties, no runtime fees, no revenue threshold.
  • Lightweight - Small download, fast startup, happy on modest machines.
  • GDScript - Python-ish and quick to prototype with. C# is there too.
  • Scene system - Everything composes from scenes. Once it clicks, reuse gets very natural.
  • Visual and written shaders - Node editor when you want it, GLSL-like code when you need control.
  • Fast iteration - The editor stays out of your way more often than not.

Weaknesses

  • 3D capabilities still maturing - Godot 4.x made huge strides in 3D rendering, but it's still behind Unity and Unreal for high-fidelity visuals.
  • Smaller ecosystem - Fewer plugins, assets, and tutorials compared to Unity or Unreal. The community is growing fast, but gaps remain.
  • Console deployment - Exporting to consoles needs third-party solutions or direct platform relationships. This is improving but not yet seamless.
  • Fewer enterprise tools - No built-in analytics, crash reporting, or cloud services compared to Unity/Unreal.

TA Workflow: Custom Visual Shader In Godot

Godot shader code is close enough to GLSL that most shader folks settle in quickly. Here is a stylized water shader, because every engine guide eventually owes you water.

GLSL
shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back;

uniform vec4 shallow_color : source_color = vec4(0.1, 0.5, 0.7, 0.8);
uniform vec4 deep_color : source_color = vec4(0.0, 0.1, 0.3, 0.95);
uniform float wave_speed : hint_range(0.0, 5.0) = 1.5;
uniform float wave_scale : hint_range(0.0, 20.0) = 8.0;
uniform float wave_height : hint_range(0.0, 1.0) = 0.15;
uniform sampler2D wave_noise : hint_default_white;
uniform float foam_threshold : hint_range(0.0, 1.0) = 0.7;

void vertex() {
 // Sample noise texture for wave displacement
 vec2 uv_offset = vec2(TIME * wave_speed * 0.1);
 float noise = texture(wave_noise, UV * wave_scale + uv_offset).r;

 // Displace vertices along normal
 VERTEX.y += noise * wave_height;
}

void fragment() {
 // Animated UV for water movement
 vec2 uv1 = UV * wave_scale + vec2(TIME * wave_speed * 0.05, 0.0);
 vec2 uv2 = UV * wave_scale * 0.8 + vec2(0.0, TIME * wave_speed * 0.03);

 float noise1 = texture(wave_noise, uv1).r;
 float noise2 = texture(wave_noise, uv2).r;
 float combined_noise = (noise1 + noise2) * 0.5;

 // Blend between shallow and deep colors based on noise
 vec4 water_color = mix(shallow_color, deep_color, combined_noise);

 // Add foam at wave peaks
 float foam = step(foam_threshold, combined_noise);
 water_color = mix(water_color, vec4(1.0), foam * 0.6);

 ALBEDO = water_color.rgb;
 ALPHA = water_color.a;
 ROUGHNESS = 0.1;
 METALLIC = 0.3;
 NORMAL_MAP = vec3(noise1 * 2.0 - 1.0, noise2 * 2.0 - 1.0, 1.0);
}
Note

Godot shaders use GLSL-like syntax with engine built-ins such as TIME, UV, and VERTEX. If you know GLSL or HLSL, the language isn't the hard part. Learning the engine's render expectations is.

TA Workflow: Procedural Placement Tool In Gdscript

This GDScript example is a classic TA helper: scatter objects on a surface in-editor, with a reroll button and artist-facing controls.

Python
@tool
extends Node3D

@export var scatter_count: int = 50
@export var scatter_radius: float = 20.0
@export var mesh_scene: PackedScene
@export var min_scale: float = 0.8
@export var max_scale: float = 1.2
@export var align_to_surface: bool = true
@export var randomize_rotation: bool = true

@export_category("Actions")
@export var regenerate: bool = false:
 set(value):
 if value:
 _scatter_objects()

func _scatter_objects() -> void:
 # Clear existing children
 for child in get_children():
 child.queue_free()

 if not mesh_scene:
 push_warning("No mesh scene assigned!")
 return

 var space_state = get_world_3d().direct_space_state
 var rng = RandomNumberGenerator.new()
 rng.randomize()

 for i in range(scatter_count):
 # Random position within radius
 var angle = rng.randf() * TAU
 var dist = sqrt(rng.randf()) * scatter_radius
 var x = cos(angle) * dist
 var z = sin(angle) * dist

 # Raycast down to find surface
 var from = global_position + Vector3(x, 50.0, z)
 var to = global_position + Vector3(x, -50.0, z)
 var query = PhysicsRayQueryParameters3D.create(from, to)
 var result = space_state.intersect_ray(query)

 if result:
 var instance = mesh_scene.instantiate()
 add_child(instance)
 instance.owner = get_tree().edited_scene_root
 instance.global_position = result.position

 # Random scale
 var s = rng.randf_range(min_scale, max_scale)
 instance.scale = Vector3(s, s, s)

 # Align to surface normal
 if align_to_surface and result.normal != Vector3.UP:
 var up = result.normal
 var forward = up.cross(Vector3.RIGHT).normalized()
 var right = forward.cross(up).normalized()
 instance.global_transform.basis = Basis(right, up, forward)

 # Random Y rotation
 if randomize_rotation:
 instance.rotate_y(rng.randf() * TAU)

 print("Scattered %d objects" % get_child_count())

3.4Open-Source Alternatives

Past Godot, the open-source engines get more specialized. They're less plug-and-ship, but they can be interesting if your project has unusual needs:

Bevy (Rust)

  • Language - Rust. Memory safety and performance without garbage collection.
  • Architecture - Entity Component System (ECS) from the ground up. Excellent for data-oriented design.
  • Status - Pre-1.0, actively developed. Not ready for complex shipped games yet, but very promising.
  • TA angle - If you want to build custom rendering pipelines from scratch with maximum performance, Bevy's architecture is appealing.

Stride (C#)

  • Language - C#. Familiar for Unity developers.
  • Features - PBR rendering, VR support, and a scene editor. Originally Xenko, developed by Silicon Studio.
  • Status - Stable but small community. Good for developers who want Unity-like C# workflows without Unity's licensing.
  • TA angle - Includes a shader editor and flexible rendering pipeline. Less mature than Unity's SRP but fully customizable.

O3de (Open 3D Engine)

  • Language - C++ with Lua and Python scripting.
  • Origin - Forked from Amazon's Lumberyard. Maintained by the Linux Foundation.
  • Features - Atom renderer, modular architecture, multi-platform support.
  • TA angle - Python scripting for pipeline tools, modular architecture for custom workflows. Still finding its community.
Note

Honest take: these are for people who want to poke engine internals, contribute upstream, or solve a problem the big three genuinely don't. If the goal is shipping an indie game soon, Unity, Unreal, or Godot is usually the calmer choice.

3.5Choosing The Right Engine

Don't choose an engine because the internet is yelling this month. Choose based on the game in front of you:

  • 2D pixel art or simple 2D game -> Godot. Lightweight, fast iteration, excellent 2D support.
  • 3D game with stylized art -> Unity (URP) or Godot 4. Both handle stylized 3D well with good shader tooling.
  • High-fidelity 3D or open world -> Unreal Engine. Nanite, Lumen, and World Partition are purpose-built for this.
  • Mobile game -> Unity. The strongest mobile pipeline and largest mobile game market share.
  • VR/AR project -> Unity or Unreal. Both have mature XR frameworks. Unity has a slight edge in mobile VR.
  • Learning/prototyping -> Godot. Fastest to get started, lowest friction, completely free.
  • Custom rendering research -> Unreal (source access) or Bevy (build from scratch).
Tip

Spend a week prototyping before you commit. One character, one lit space, one shader, one mechanic, one build. That tiny slice will reveal more than ten comparison articles.

3.6TA-Specific Features Comparison

Here is the TA-flavored version of the comparison: shaders, tools, imports, VFX, and how much control you get before the engine starts saying no.

Shader Authoring

  • Unity - Shader Graph (node-based) + HLSL for custom shaders. URP and HDRP have different shader models. ShaderLab wrapping syntax is unique to Unity.
  • Unreal - Material Editor (node-based) is best-in-class. Custom HLSL nodes possible. Material functions and parameter collections enable reusable, data-driven materials.
  • Godot - Visual Shader Editor + GLSL-like shading language. Less powerful than Unreal's Material Editor but very accessible and quick to iterate with.

Editor Scripting & Tool Building

  • Unity - C# editor scripting with full access to the editor API. Custom inspectors, windows, and toolbars. UI Toolkit or IMGUI for editor UI.
  • Unreal - Editor Utility Widgets (Blueprints or C++). Python scripting for pipeline automation. Slate framework for custom UI.
  • Godot - @tool annotation runs scripts in the editor. EditorPlugin API for custom docks and inspectors. GDScript or C# for tooling.

Pipeline & Asset Management

  • Unity - AssetDatabase API, custom importers, ScriptedImporter for custom file formats. Addressable assets for runtime loading.
  • Unreal - Asset registry, Data Assets, custom import factories. Unreal's asset pipeline is the most solid for large projects.
  • Godot - ResourceLoader/ResourceSaver system. Custom import plugins via EditorImportPlugin. Simpler but sufficient for most indie projects.

Vfx Systems

  • Unity - VFX Graph (GPU-driven, node-based) for complex effects. Particle System (Shuriken) for simpler effects. Both are mature.
  • Unreal - Niagara is extremely powerful. GPU particles, mesh particles, custom data interfaces, and deep integration with materials.
  • Godot - GPUParticles3D/2D with shader-based customization. Less feature-rich than Unity VFX Graph or Niagara but functional for indie-scale projects.
Warning

Don't spend months evaluating engines. Pick the one that matches the highest-priority requirement, prototype for a week, and commit. You can build the next game in another engine. You can't ship the game you never start.

3.7Engine-Specific Scripting: Asset Validation

Every TA needs asset validation tools. Here's the same concept - checking texture sizes at import - implemented in each engine's scripting language:

Unity (C#)

Code
using UnityEditor;
using UnityEngine;

public class TextureValidator : AssetPostprocessor
{
 private const int MaxSize = 4096;

 void OnPreprocessTexture()
 {
 TextureImporter importer = (TextureImporter)assetImporter;
 Texture2D tex = AssetDatabase.LoadAssetAtPath<Texture2D>(importer.assetPath);

 if (tex != null && (tex.width > MaxSize || tex.height > MaxSize))
 {
 Debug.LogWarning(
 $"[TA] Texture '{importer.assetPath}' is {tex.width}x{tex.height}. " +
 $"Max allowed: {MaxSize}x{MaxSize}. Consider resizing.");
 }

 // Enforce power-of-two for non-UI textures
 if (!importer.assetPath.Contains("/UI/"))
 {
 importer.npotScale = TextureImporterNPOTScale.ToNearest;
 }
 }
}

Unreal (Python)

Code
import unreal

MAX_TEXTURE_SIZE = 4096

def validate_textures(directory="/Game/Textures"):
 """Scan all textures in a directory and flag oversized ones."""
 asset_registry = unreal.AssetRegistryHelpers.get_asset_registry()
 assets = asset_registry.get_assets_by_path(directory, recursive=True)

 issues = []
 for asset_data in assets:
 if asset_data.asset_class_path.asset_name != "Texture2D":
 continue

 texture = asset_data.get_asset()
 width = texture.blueprint_get_size_x()
 height = texture.blueprint_get_size_y()

 if width > MAX_TEXTURE_SIZE or height > MAX_TEXTURE_SIZE:
 issues.append({
 "path": str(asset_data.package_name),
 "size": f"{width}x{height}",
 })
 unreal.log_warning(
 f"[TA] Oversized texture: {asset_data.package_name} "
 f"({width}x{height}). Max: {MAX_TEXTURE_SIZE}"
 )

 unreal.log(f"Scanned {len(assets)} assets. Found {len(issues)} issues.")
 return issues

# Run from Unreal Python console:
# validate_textures("/Game/Textures/Environment")

Godot (Gdscript)

Code
@tool
extends EditorScript

const MAX_SIZE := 4096

func _run() -> void:
 var dir := DirAccess.open("res://assets/textures")
 if not dir:
 printerr("Could not open textures directory")
 return

 var issues := 0
 var scanned := 0
 dir.list_dir_begin()
 var file_name := dir.get_next()

 while file_name != "":
 if file_name.ends_with(".png") or file_name.ends_with(".jpg"):
 var path := "res://assets/textures/" + file_name
 var image := Image.load_from_file(path)

 if image:
 scanned += 1
 if image.get_width() > MAX_SIZE or image.get_height() > MAX_SIZE:
 issues += 1
 push_warning(
 "[TA] Oversized: %s (%dx%d). Max: %d" % [
 file_name,
 image.get_width(),
 image.get_height(),
 MAX_SIZE
 ])

 file_name = dir.get_next()

 dir.list_dir_end()
 print("Scanned %d textures. Found %d issues." % [scanned, issues])
Tip

Asset validation scripts are one of the highest-value tools a TA can build. Run them automatically on import or as a CI step. Catching oversized textures, incorrect compression settings, or missing LODs early saves hours of debugging performance issues later.