Back to Projects

Out of Time

Core-to-Unreal automation pipeline and large-scale mob rendering for a multiplayer rogue-lite

Unreal Engine 4/5World PartitionBlueprintC++PythonPowerShellJenkinsVAT
Company

Manticore Games

Year

2026

Role

Product Developer / Senior Technical Artist

Category
PIPELINE

Context

A multiplayer rogue-lite where artists iterate quickly in the Core editor while an offline pipeline I built converts static environments into native Unreal levels, and VAT rendering supports 200+ on-screen mobs to hit the Steam Deck performance target.

My Contribution

Led the design and implementation of the Core-to-UE conversion pipeline, the World Partition and Data Layer map structure, Jenkins build automation, and the VAT system for distant mobs.

Impact / Outcome

Artists kept Core's fast iteration while the runtime gained native UE performance. The pipeline became the performance foundation of Out of Time: ~50% fewer actors, ~90% fewer mob draw calls, and 200+ on-screen mobs at 30 FPS on Steam Deck.

Outcome Summary

Out of Time: Core-to-UE Pipeline and Large-Scale Mob Rendering

TL;DR: The Core editor is fast to iterate in, but large scenes carry a heavy runtime cost. I led the development of an offline Core-to-UE conversion pipeline that turns static environments into native Unreal open-world levels and became the performance foundation of Out of Time. I also designed and implemented a VAT system for distant mobs. The game now runs 200+ animated mobs on screen at 30 FPS on Steam Deck.

Medieval mapMedieval map

Table of Contents

At a Glance

Project period2023 - 2026
My roleOwner of the Core-to-UE pipeline; designed and implemented the open-world map structure and the VAT system
PlatformsPC, Steam Deck
Key numbersActors −~50% · Mob draw calls −~90% · 30 FPS with 200+ on-screen mobs on Steam Deck · 140+ mob types converted to VAT · 500+ automated builds · −80+ material instructions

The Problem

Out of Time is a multiplayer rogue-lite built on Core, centered on large era-themed wilderness maps, repeatable combat encounters, and high enemy counts.

The Core editor is designed for live editing: every mesh needs a Core-level wrapper actor, and materials and VFX stay fully editable at runtime. That makes iteration very fast, but it comes with a runtime cost. At 2k×2k map sizes with hundreds of enemies on screen, this dynamic representation could not keep up. Large maps stuttered noticeably in Core, especially in areas with heavy use of dynamic assets such as spline meshes.

This created a conflict:

  • Artists needed to keep iterating quickly in Core and see their changes immediately.
  • The runtime needed native, static, optimized UE assets.

Constraints

  • Don't change how artists work: environment artists keep building maps in the Core editor, and the pipeline stays invisible to them.
  • Maps are constantly iterated: the conversion must be repeatable and automated, not a one-time manual migration.
  • Not everything can be converted: gameplay systems rely on Core's built-in features, so only non-interactive environment assets are candidates for static conversion.
  • Steam Deck is a target platform.
  • Two source control systems: Git on the Core side, Perforce on the UE side.

Optimization 1: Map Structure

Each wilderness map is 2k×2k, with one map per era and four zones per map.

Why one large map per era: all four zones share the same era, so keeping them in one map makes visual consistency easier. Shared assets only need to exist once, each zone simply hides content that should not be visible, and the artist workflow stays simpler.

Alternative considered: one map per zone. Shared assets would have to be duplicated across zones, vistas could not be shared, and every environment change would have to be repeated across multiple maps. That would make consistency harder, add work, and make any change to player routes expensive.

The structure:

  • World Partition is the base loading strategy. Zone content is distributed across separate Data Layers, so each zone loads only what it needs.
  • A shared "always-on" Data Layer holds vista objects visible from all four zones.
  • Recognizable structures such as buildings, castles, and bridges are packed as Packed Level Actors or Level Instances.
  • Sparse props are split into separate Level Instances by the World Partition cell grid.
  • Layered HLODs handle distant objects beyond the player's immediate view.

Optimization 2: Core-to-UE Conversion Pipeline

Core map vs. UE map structureCore map vs. UE map structure

How It Works

In a Core map, each group of assets to be converted sits under a custom node called a CUE node. The pipeline processes everything under each CUE node and converts it into native UE level assets, including static meshes, spline meshes, decals, VFX, sound effects, and lights. Each asset type has its own conversion rules. The core mechanism: capture the state of every asset during a UE Play-In-Editor (PIE) session, then write it out to a native level.

Why capture in PIE instead of parsing Core map data directly: the final state of a Core object is only fully determined at runtime (script-generated content, parameterized materials, and so on), so offline parsing struggles to produce consistent results. In PIE, all of an asset's current data is directly available, and it can be saved straight into new native UE assets, which makes rebuilding straightforward.

Pipeline overviewPipeline overview

Iteration Workflow

In Core, environment changes are visible immediately after republishing. In UE, every change requires a full build cycle. So whenever an environment artist updates a zone, the conversion has to be re-run for that era. To reduce run time, I added change detection so the pipeline only processes what changed, which greatly shortened iteration time.

Iteration stepsIteration steps

The whole process runs automatically as a Jenkins job. Each map has its own job that can be queued independently. The pipeline pulls updates from Git, identifies the changed CUE nodes, runs an incremental conversion, and submits the updated UE assets to Perforce automatically. It can also run HLOD builds automatically.

  • Run time: from about 20 minutes for an incremental run to about 4 hours for a full run, plus HLOD build time
  • Successful runs: 500+
  • Failure handling: Slack notification with a log link and the name of the failing CUE node. I documented common errors so they can be diagnosed quickly, and I investigate the rare edge cases directly.

Automated Optimization Passes

Every rebuild applies the following passes:

OptimizationApproachResult
Remove wrapper actors from all environment assetsCapture a data table of the native UE assets each wrapper references, then rebuild using the original assets~50% fewer actors
Mesh instancingAutomatically instance static meshes that share the same asset, across Packed Level ActorsLarge draw-call reduction
Actor mergingMerge actors made of many small parts into a single mesh, then instance itLarge draw-call reduction
Spline mesh mergingMerge Core's editable spline meshes into static meshesAffected areas went from noticeable stutter to 40+ FPS
Material simplificationSave dynamic materials as deduplicated static material instances, reduce instruction counts, use more static switches, and strip logic branches only needed in the Core editor80+ fewer instructions on average for eligible materials
Blueprint data baking (selective)Bake runtime data into blueprints to remove dynamic logic where possibleLower CPU cost

Optimization 3: VAT for Distant Mobs

Rendering many animated skeletal meshes at once is expensive. Beyond a certain threshold, we switch mobs to Vertex Animated Textures (VATs): each mob's attack and run cycles are baked into a texture, and a vertex shader on a static mesh reproduces the skeletal animation.

Key Design Decisions

  • When to switch: driven by UE's Significance Manager, which scores each mob by distance and screen size and switches it to VAT below a threshold. On Steam Deck, where the CPU is the bottleneck and the loss of close-range animation fidelity is barely noticeable to players, all mobs use VAT.
  • Avoiding popping: the switch happens within a single frame, with animation phase aligned.
  • Baked animations: attack and run.
  • Animation textures: 8K, storing bone position, rotation, and weight. All instances of the same mob type share one texture.
  • Baking workflow: UE's built-in Animation to Texture plugin, run either as a batch over all mobs or on a single mob. Each bake takes about 10 seconds.

In total, 140+ mob types were processed. Every non-boss mob went through the VAT pipeline.

Results

  • Replacing skeletal meshes with VAT meshes reduced draw calls by ~90%. Because of the high mob counts, we also applied instancing, collision adjustments, and lighting setup to the VAT meshes, reaching the Steam Deck target of 30 FPS.
  • 200+ animated mobs can stay on screen at once.

My Contribution

Owned end to end:

  • Open-world map setup and comparison, plus HLOD setup and testing
  • Overall design and implementation of the Core-to-UE pipeline
  • Design and implementation of the conversion logic for every asset type in the pipeline
  • Primary maintenance of the pipeline and its tools
  • Implementation of the VAT system and processing of all mob assets

Led, in collaboration with others:

  • Defined the CUE node specification with engineers and artists
  • Worked with engineers on Jenkins integration, including editor launch and map loading in the automated flow
  • Defined mesh-merging rules and scope with artists
  • Ran A/B tests on selected features with engineers

Supported work led by others:

  • Engineers implemented automatic static mesh instancing across Packed Level Actors; I provided test validation and pipeline integration

Retrospective: What I Would Do Differently

  • Define per-zone performance budgets earlier (draw calls, triangles, memory), instead of hunting for problems during the optimization phase.
  • Build a reproducible benchmark for every optimization: we tested the most uncertain optimizations on target hardware using fixed camera paths, but not all of them, which is why a few rows in the table above can only say "large reduction." Next time, I would measure frame time and draw calls for each optimization individually, both to quantify its benefit and to see which ones give the best return on effort.
  • Push for upstream improvements: the conversion pipeline essentially works around Core's runtime overhead downstream. Having built it, I would push for native "mark as static and bake" support in Core, so this kind of optimization becomes a platform capability that other Core games can benefit from too.
  • Catch errors on the Core side: today, a failed conversion only surfaces as a Slack notification after the Jenkins job finishes. A pre-flight check in the Core editor, flagging unsupported asset types and non-convertible materials under CUE nodes, would let artists catch problems before submitting and save a wait that can run to several hours.