{"id":321,"date":"2026-05-15T08:39:58","date_gmt":"2026-05-15T15:39:58","guid":{"rendered":"https:\/\/secondhandcarrot.com\/blog\/?p=321"},"modified":"2026-05-15T08:40:04","modified_gmt":"2026-05-15T15:40:04","slug":"body-doubles","status":"publish","type":"post","link":"https:\/\/secondhandcarrot.com\/blog\/dev\/body-doubles\/","title":{"rendered":"Body Doubles"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">The last post was about the invisible part \u2014 the abstract simulation that keeps a town breathing even when the player isn&#8217;t there. This post is about what happens when the player <em>is<\/em> there, and one of those simulated NPCs needs a body.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We use Unreal&#8217;s Mass Representation system to handle that handoff. When an NPC is far away, it&#8217;s just data \u2014 a position, a destination, a small bundle of fragments riding the ECS train. When the player gets close, Mass spawns an Actor to represent that entity. When the player leaves, the Actor goes away and the data keeps simulating.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That sounds simple. It is not.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The first version of our NPC pipeline used placed Actors \u2014 characters dropped into the level by hand or spawned from a subsystem at startup. They were full Actors all the time, AIControllers and Behavior Trees and all, regardless of whether the player could see them. That worked at small scale. At village-scale it absolutely did not.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So this week the job was tearing that apart and reseating everything on Mass:<\/p>\n\n\n\n<div class=\"wp-block-group is-layout-constrained wp-block-group-is-layout-constrained\">\n<pre class=\"wp-block-code has-ast-global-color-3-color has-ast-global-color-5-background-color has-text-color has-background has-link-color wp-elements-8638ef0e5baeb34607bc3360f622deb5\"><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502   Mass Entity (data only)  \u2502\n\u2502   FAR FROM PLAYER          \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n               \u2502  player gets close\n               \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502   Spawn Actor (the puppet) \u2502\n\u2502   - copy mesh + state      \u2502\n\u2502   - wire movement comp     \u2502\n\u2502   - parent to entity       \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n               \u2502  player leaves\n               \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502   Despawn Actor            \u2502\n\u2502   Entity keeps simulating  \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/pre>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The interesting bit \u2014 the part that ate most of the week \u2014 is the wiring step. Mass doesn&#8217;t just spawn an Actor and walk away. Several fragments inside the entity need to be told <em>which<\/em> component on the spawned Actor they own: the movement component, the capsule, the scene root, the agent radius. If any of those wires are missing or stale, the Actor and the entity will gracefully disagree about where the character is \u2014 the entity off doing its errand, the puppet rooted to the spawn spot.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We had a version that did exactly that. The entity would walk to the coffee shop. The Actor would just stand there looking dumb.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fix was a custom <code>UMassRepresentationActorManagement<\/code> subclass that takes over the post-spawn step. It manually wires the wrapper fragments to the puppet&#8217;s components, sets the movement mode and a couple of physics flags the engine doesn&#8217;t set automatically when there&#8217;s no PlayerController in the loop, and \u2014 importantly \u2014 does <em>not<\/em> re-run construction scripts on the spawned Actor. Re-running construction was the silent killer: it would reset the movement component back to defaults, and then the entity and the puppet would drift.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There were also some quality-of-life problems to clean up along the way. The State Tree tasks driving NPC behavior were too eager to fail when their data wasn&#8217;t ready yet. An NPC waking up at 6am before SmartObjects had finished loading would emit a hard error and stop for the day. The new behavior is: park in Running, log at verbose, try again next tick. The number of NPCs in the village that are now patient with each other has gone up dramatically.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On the side, we ended up writing some asset cleanup tooling. A year of importing marketplace packs from multiple vendors means we&#8217;ve accumulated duplicate <code>.uasset<\/code> files scattered across the project \u2014 the same prop shipped in three different bundles, the same texture re-saved under a slightly different name. The new audit script hashes every asset under <code>\/Game\/<\/code>, groups by content, and produces a CSV of duplicates sorted by reclaimable disk. The first dry run found more than I want to admit. That&#8217;s a job for a future post.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The takeaway is the same as last time: abstraction has a cost. Mass keeps the town breathing on a handful of frames per second. Mass Representation makes those breathing characters become actual breathing characters when you walk up to them. The seam between the two is where most of the bugs live, and most of the week was spent down there with a soldering iron.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Worth it though. When it works, you don&#8217;t notice. Which is the whole point.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The last post was about the invisible part \u2014 the abstract simulation that keeps a town breathing even when the [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":31,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"give_campaign_id":0,"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"disabled","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"advanced_seo_description":"Discover how Unreal's Mass Representation system transforms NPC data into lifelike characters as players approach, enhancing game immersion.","jetpack_seo_html_title":"How Unreal's Mass Representation Transforms NPCs in Gaming","jetpack_seo_noindex":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"edge","default_image_id":27,"font":"","enabled":true,"token":"eyJpbWciOiJodHRwczpcL1wvc2Vjb25kaGFuZGNhcnJvdC5jb21cL2Jsb2dcL3dwLWNvbnRlbnRcL3VwbG9hZHNcLzIwMjZcLzA0XC9jcm9wcGVkLWNyb3BwZWQtMkhDTG9nby5wbmciLCJ0eHQiOiJCb2R5IERvdWJsZXMiLCJ0ZW1wbGF0ZSI6ImVkZ2UiLCJmb250IjoiIiwiYmxvZ19pZCI6MjU0MTM4ODU3fQ.l8A4OL1EdRpOYL0Kk4SHDb4eCm0e8lOhKn2Rhs5RH1cMQ"},"version":2},"_wpas_customize_per_network":false,"jetpack_post_was_ever_published":false},"categories":[9],"tags":[31,8,32,14,13],"class_list":["post-321","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dev","tag-code","tag-game","tag-simulation","tag-tech","tag-unreal"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/secondhandcarrot.com\/blog\/wp-content\/uploads\/2026\/04\/cropped-cropped-2HCLogo.png?fit=512%2C512&quality=80&ssl=1","jetpack_likes_enabled":true,"jetpack_sharing_enabled":true,"jetpack-related-posts":[],"_links":{"self":[{"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/posts\/321","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/comments?post=321"}],"version-history":[{"count":1,"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/posts\/321\/revisions"}],"predecessor-version":[{"id":322,"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/posts\/321\/revisions\/322"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/media\/31"}],"wp:attachment":[{"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/media?parent=321"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/categories?post=321"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/tags?post=321"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}