Skip to content
The Technical ArtistArt. Code. Pipelines. Games.
Home/Indie Dev/Game Engines
article 03
indie dev

Choosing A Game Engine

Workingguide~9 min read

Compare engines through the work you'll do: importing assets, writing tools, making shaders, testing builds, and keeping iteration practical.

On this page UnityUnreal EngineGodotOpen-Source AlternativesChoosing The Right EngineTA-Specific Features ComparisonEngine-Specific Scripting: Asset Validation

Choose an engine by testing the parts of your game that carry the most risk. For a TA, that includes imports, editor scripting, shader workflows, and the time it takes the team to see a change in a build.

Tip

There isn't one best engine for every project. Shortlist the options that fit your team, targets, and budget, then try a small representative task in each.

3.1Unity

Unity supports a broad range of 2D and 3D projects. Its C# editor scripting and package ecosystem are useful for TA workflows. Test the render pipeline and target-platform build you intend to use, since that matters more than a general feature list.

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: test representative content on the target hardware. The amount of optimisation needed depends on your scene, systems, and chosen render pipeline.
  • 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 has extensive material, rendering, and editor tools. Blueprints and Python support many TA workflows, while source access gives teams another route when the built-in options aren't enough. Allow time to learn the project's asset and build conventions.

Strengths

  • Rendering features: test Nanite, Lumen, and virtual shadow maps against the visual target and hardware budget.
  • Material Editor: node-based authoring with reusable functions, instances, and custom HLSL where needed.
  • Blueprints - Great for artist-facing actors, prototypes, editor utilities, and gameplay glue.
  • Source access: engine changes are possible, but they add development and maintenance work. Check whether a plugin or existing extension point would be sufficient first.
  • Niagara - Deep VFX system with GPU simulation, mesh particles, and custom data interfaces.

Weaknesses

  • Learning and setup: allow time for asset references, project settings, builds, and the editor's conventions.
  • C++: some runtime features and integrations need C++ alongside Blueprints.
  • Build and shader compilation: measure iteration time on the hardware the team will use.
  • Licensing: check Epic's current licence for your product, including royalty thresholds, exclusions, and any applicable seat requirements. Don't assume the same terms apply to every use case.

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 let several materials read shared values. They can be useful for weather or time-of-day controls when those values should change together.

3.3Godot

Godot is an open-source option with a relatively lightweight editor and both 2D and 3D workflows. Try the rendering, tools, and deployment path your project needs before choosing it.

Strengths

  • Open-source licence: Godot uses the MIT licence. Check the licence requirements for the engine and any third-party assets or plugins you add.
  • 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

  • Rendering requirements: test your intended materials, lighting, and scene scale on target hardware. A feature checklist won't tell you whether the visual result and frame time fit.
  • 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's shader language uses GLSL-like syntax. The example below builds stylised water and gives you a place to explore the engine's built-in inputs.

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

Other open-source engines may fit projects with particular technical needs. Include editor tooling, deployment, support, and maintenance in the comparison:

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.
  • Project readiness: check the current release, supported platforms, and migration workload against what your team can maintain.
  • TA angle: useful to investigate if the project needs custom systems and the team is comfortable developing them in Rust.

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 customisable.

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

These engines may suit teams that want to build or maintain more of the underlying technology. If your priority is finishing a game with a small team, include that extra work in the estimate before choosing one.

3.5Choosing The Right Engine

Start with the project's most important requirements, then use these as candidates for a prototype:

  • 2D pixel art or simple 2D game -> Godot. Lightweight, fast iteration, excellent 2D support.
  • 3D game with stylised art -> Unity (URP) or Godot 4. Both handle stylised 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: include Unity in the shortlist and test a representative build on the actual devices you need to support.
  • VR/AR project -> Unity or Unreal. Both have mature XR frameworks. Unity has a slight edge in mobile VR.
  • Learning or prototyping: Godot is worth trying if you want a small editor and a straightforward setup.
  • 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

The comparison below focuses on material authoring, tools, imports, and effects. Check the details against the engine version and render pipeline you'll use.

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 with custom HLSL nodes, material functions, instances, and parameter collections for reusable 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, and custom import factories support pipeline tooling. Test the workflow with representative project content.
  • 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 supports GPU particles, mesh particles, custom data interfaces, and integration with materials.
  • Godot - GPUParticles3D/2D with shader-based customisation. Less feature-rich than Unity VFX Graph or Niagara but functional for indie-scale projects.
Warning

Set a time limit for the evaluation and test the requirements most likely to affect delivery. Record the limitations you find, choose an option the team can support, and move on to building the game.

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

Automated checks can catch oversized textures, unsuitable compression settings, or missing data before they reach a build. Run them at an appropriate import or CI step and make each error explain what needs attention.