<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Sam Bloomberg]]></title><description><![CDATA[Code, Games, and Thoughts]]></description><link>https://xbloom.io/</link><image><url>https://xbloom.io/favicon.png</url><title>Sam Bloomberg</title><link>https://xbloom.io/</link></image><generator>Ghost 5.84</generator><lastBuildDate>Tue, 09 Jun 2026 07:29:30 GMT</lastBuildDate><atom:link href="https://xbloom.io/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Unreal's World Partition: Internals]]></title><description><![CDATA[<p>This is a sort of stream-of-consciousness document explaining how world partition works under the hood.</p><p>This is not a general overview of world partition, nor is it an explanation of how to use world partition or terms used in the user-facing parts of it. See Epic&apos;s own docs</p>]]></description><link>https://xbloom.io/2025/10/24/unreals-world-partition-internals/</link><guid isPermaLink="false">68f03ab7dc62ea03368d97ad</guid><dc:creator><![CDATA[Sam Bloomberg]]></dc:creator><pubDate>Fri, 24 Oct 2025 00:09:24 GMT</pubDate><content:encoded><![CDATA[<p>This is a sort of stream-of-consciousness document explaining how world partition works under the hood.</p><p>This is not a general overview of world partition, nor is it an explanation of how to use world partition or terms used in the user-facing parts of it. See Epic&apos;s own docs for that - this is meant to cover the &quot;undocumented&quot; innards of world partition.</p><h1 id="top-level-concepts">Top-Level Concepts</h1><p>World partition at runtime is &quot;just&quot; normal level streaming, but with a ton of additional features built on top to make managing that streaming much easier. Being built on normal level streaming, actors are not loaded individually (that&apos;d be slow) - instead actors are grouped based on the runtime grid and data layers they are placed into at edit-time and also based on what references they have to other actors.</p><h2 id="streaming">Streaming</h2><p><strong>Runtime grids</strong> (defined in world settings) specify how actors get placed into <strong>grid cells</strong>, and each cell results in one or more <strong>streaming levels</strong> being created during cook (and PIE!) for use at runtime. Streaming levels are also generated for combinations of <strong>runtime data layers</strong> that overlap within a single cell, meaning multiple streaming levels may generate for one physical location if there are actors in different runtime grids or data layers. As such, having lots of &quot;overlapping&quot; data layers and grids is harmful to streaming performance. On the flip side, a space with no actors in a grid or data layer will not generate a streaming cell and has no overhead - grids and data layers for the most part only have streaming costs in locations that contain actors.</p><p><strong>Actor clusters</strong> are groups of actors that have hard references to each other and thus must always be loaded within the same streaming level in order to guarantee those references stay intact.</p><p>Actors and actor clusters larger than a cell or too far across multiple cell boundaries may be &quot;promoted&quot; into a higher grid level, which is a cell that encompasses a larger area. As such, even within a single runtime grid you can have multiple streaming levels overlapping each other due to (among other reasons) grid promotion.</p><div class="kg-card kg-callout-card kg-callout-card-red"><div class="kg-callout-emoji">&#x26A0;&#xFE0F;</div><div class="kg-callout-text">Because hard references are taken into account to generate actor clusters, an actor that hard references another actor across the entire map (regardless of how far) will end up being clustered together into a massive cell that will be loaded any time you are anywhere between the two actors, even if nothing else around them is loaded.<br><br>Soft references do not have this behavior since they do not require their target object to be loaded at the same time.</div></div><p></p><p>Runtime grids are defined as part of the world partition <strong>runtime hash</strong>, which is a class that decides the layout of the runtime grid through the use of a spatial hash. Spatial hashes are selected as part of world partition settings - the current default as of the time of writing is the <strong>LH Grid</strong>, which attempts to place actors into a regular grid but will expand/shrink the boundaries of cells if the cell would be slightly over- or under-sized.</p><p>The spatial hash is effectively a function that takes any location in the world and then outputs the name of the streaming cell that corresponds to that location. At cook time this is used to generate streaming levels, at runtime it is used to decide which levels to stream for a particular location.</p><p>The way that the engine decides what locations to feed into the streaming system at runtime is controlled by two main APIs: <strong>streaming sources</strong> and <strong>data layers</strong>. Streaming sources allow objects like the player controller to register themselves as a location that the world should stream from, including options such as the radius and target runtime grid(s) to have some control over what exactly gets streamed in.</p><p><strong>Runtime </strong>data layers are a complimentary method which allow controlling groups of actors to load and unload as defined in the editor. Actors on a data layer still obey spatial loading settings and will only load if non-spatially-loaded or if a streaming source tells them to. <strong>Editor</strong> data layers only exist within the editor and are primarily an organizational tool, though it is possible to build other editor tools on top of them (such as actor filters,).</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Data layers work even in world partition levels with streaming disabled - they can be very useful even if you don&apos;t need spatial streaming!</div></div><p></p><p><strong>External data layers</strong> (EDLs) are data layers generally registered by game feature plugins. Content placed into an EDL is stored inside the owning plugin&apos;s content folder instead of in the owning level&apos;s content folder. This allows completely unregistering the content inside the EDL from the engine - you can simply disable/not cook the owning plugin and the EDL&apos;s content won&apos;t be packaged. This is particularly useful for DLCs or for live-service content like Fortnite uses (for example, a seasonal event that won&apos;t be going out in the same release as the map it takes place on, or one that will need to &quot;inject&quot; content for a limited period of time between client updates).</p><p><strong>Content bundles</strong> are effectively an older implementation of EDLs with less features and a worse API and are considered legacy.</p><h2 id="level-instances">Level Instances</h2><p><strong>Level instances</strong> are levels that have been placed into a world like a normal actor - sort of like a sublevel, but you can have multiple copies of the same level instance scattered across the world. At runtime, only <strong>standalone</strong> level instances exist - <strong>embedded </strong>level instances are broken apart at cook time (and thus have no runtime representation or overhead, aside from the actors broken out of them), while standalone instances stream as a unit even at runtime. This can have some benefits, like being able to manually control the streaming of an entire level instance.</p><div class="kg-card kg-callout-card kg-callout-card-red"><div class="kg-callout-emoji">&#x26A0;&#xFE0F;</div><div class="kg-callout-text">EDLs do <b><strong style="white-space: pre-wrap;">NOT</strong></b> support level instances - EDLs cannot be used inside level instances, nor can level instances be used inside EDLs.<br><br>This is a pretty major limitation. It is possible to modify the the engine to support both of these scenarios (and I have successfully done so for a project), but it requires some very complex modifications in very easy to break locations. See the end of this post for an overview of what is necessary to fix this, though note that it is not an easy task.</div></div><p></p><p><strong>Sub-world partitions</strong> are standalone level instances that point to a world partition level that has streaming enabled. This lets you define entirely separate streaming settings for that level instance and have it stream just like it would if you were loaded into it directly. Note that this can be more expensive, and that if the level instance itself is not streamed then nothing within it will be either. That includes non-spatially-loaded actors - they will only load if the outer level instance is loaded!</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">As of 5.6, it is possible to have the engine &quot;extract&quot; HLODs from a sub-world-partition into a separate level that gets embedded into the outer world - this allows the HLODs to be loaded even when the sub-world-partition isn&apos;t.</div></div><p></p><h1 id="in-the-editor">In the Editor</h1><p>The editor does not operate on the same concepts as runtime does as the runtime representation of the world requires having cooked all actors into streaming levels. Instead, the editor needs to deal with each actor individually and has an entire data model for working with actors without having to actually load them.</p><p>The entire system is built on top of the asset registry, where metadata about each actor is serialized as part of asset registry tags. Asset registry tags do not require explicit loading and thus provide a fast way for the editor to learn important information about an actor such as its label, position, bounds, class, etc - everything you need to be able to display the actor in the outliner and decide whether to load it on the world partition minimap.</p><p>Note that this reliance on the asset registry means that sometimes world partition data in the editor can become stale - this usually results in &quot;ghost&quot; actors appearing in the outliner that can&apos;t actually be loaded. This isn&apos;t a big deal and can be ignored, but clearing your asset registry cache can sometimes help resolve it.</p><h2 id="descriptors">Descriptors</h2><p><strong>Actor descriptors</strong> (<code>FWorldPartitionActorDesc</code>) contain all data about an actor that is needed by the editor when that actor hasn&apos;t yet been loaded. These are serialized and then transformed into a base64-encoded string that is saved into an asset registry tag on the uasset that contains the actor it represents. The default descriptor type includes the actor label, runtime and editor bounds, data layer assignments, content bundles, and other data required by the editor.</p><p>It is possible to create your own custom actor descriptor subclass to allow storing out whatever editor-only data you desire to ingest in your own tools. This involves creating a subclass of <code>FWorldPartitionActorDesc</code>  and then implementing <code>CreateClassActorDesc</code> on the actor to return your new actor descriptor.</p><p><strong>Actor Descriptor Containers </strong>(<code>UActorDescContainer</code>) contain a list of actor descriptors that generally are fed to streaming generation as a unit - for example, all of the actors in a basic world partition level will be placed into a single top-level container that represents that level. Every external data layer in that level will then create an additional container to store all of the actors within that EDL, since those actors are stored and processed from a separate location.</p><p>Containers are not &quot;saved&quot; anywhere and cannot store any persistent data - they are created on the fly by the editor from each directory that world partition actors are stored in. You can generally think of a container as an editor representation of a single level&apos;s directory in the <code>__ExternalActors__</code> folder of the project or a plugin&apos;s content directory.</p><p>It is possible to make a custom container subclass but this is unusual - currently the only additional implementation of a container in the engine is for property overrides for level instances (a feature that Epic has sadly abandoned because of SceneGraph... which is something that does not yet exist for anyone outside of Fortnite and is thus not particularly useful).</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text"><code spellcheck="false" style="white-space: pre-wrap;">__ExternalActors__</code> is where one-file-per-actor (OFPA) assets get stored - every uasset in this folder is a single actor (plans for one-file-multiple-actors for PCG use notwithstanding). The initial directories under this folder are named based on the path to the level the actors belong to. Digging deep enough into these folders, you&apos;ll eventually encounter more folders with seemingly random letters and numbers, and the assets themselves will also have random names. These names are not significant and are randomly generated just to avoid version control conflicts - if the filenames were based on actor name or location then you&apos;d hit a conflict just because two people placed actors in a similar way.<br><br>The <code spellcheck="false" style="white-space: pre-wrap;">__ExternalObjects__</code> directory is effectively the same, but it is used for objects referenced by a level that aren&apos;t actors.<br><br>Additionally these folders can have certain &quot;special&quot; folders at the top level - for example, there will be an <code spellcheck="false" style="white-space: pre-wrap;">EDL</code> folder for the storage of all actors and objects belonging to an external data layer owned by that plugin.</div></div><p></p><p>Actor descriptors represent an actor saved to disk, but they do not represent the actual placement of that actor into a level - for that we have <strong>actor instance descriptors </strong>(<code>FWorldPartitionActorDescInstance</code><strong>)</strong>. These are <em>not</em> saved to disk but instead are basically a handle to an actor descriptor along with information about the placement of that actor and what level owns it. This is important because <strong>embedded level instances</strong> can result in the same on-disk actor being instanced multiple times into the same level. Actor instance descriptors cannot be subclassed and do not carry any &quot;custom&quot; data.</p><p><strong>Actor descriptor container instances</strong> (<code>UActorDescContainerInstance</code>contain a list of actor instance descriptors, a reference to the container that the original descriptors are in, and a list of sub-container instances.</p><p>Container instances are used for a variety of reasons - a basic level may only have a single container instance and a single container, but every level instance will result in a new container instance. Furthermore, actor filters on a level instance may change which actor descriptor instances get created for a container instance.</p><p>Container instances are hierarchical, so one container instance can contain another container instance. This is how the hierarchy of nested level instances works - you have the outermost level instance parented to the level&apos;s root container instance, and then every level instance inside it is a sub-container, creating a hierarchy that mimics the placement of the level instances themselves.</p><p>Custom container instance subclasses are also possible by subclassing <code>UActorDescContainerInstance</code>, though this is again somewhat unusual. Container instances aside from the root are created through actor descriptors (see <code>IsChildContainerInstance</code> and related functions) - thus any actor descriptor can in theory provide an additional container instance to the level.</p><h2 id="streaming-generation">Streaming Generation</h2><p>Streaming generation is the process in which the editor takes the above &quot;unloaded&quot; data model for the world and generates a list of streaming levels (and what goes in them) for use at runtime. This process is used not only during cooking for saving out the streaming level packages, but also to generate the world on the fly in PIE.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Note that in PIE the entire world does not get loaded: streaming generation must run for the entire world (which can be somewhat slow on very large worlds), but streaming levels are only created and actors are only loaded on an as-needed basis.</div></div><p></p><p>Streaming generation is an incredibly complex topic in and of itself, but it boils down to the following steps:</p><ol><li>Collect the list of container instances that will generate streaming levels</li><li>Filter actors within each container (remove editor-only actors, for example)</li><li>Cluster actors based on how big they are, what runtime grid they are in, what they hard-reference, and what data layers they belong to</li><li>Feed the sets of actor clusters to the configured spatial hashes to decide on the list of streaming levels to generate and which actors fit in each.</li></ol><p>This process can be tested in-editor with the command <code>wp.Editor.DumpStreamingGenerationLog</code> which will run streaming generation on the active level and output a log of the results to <code>Saved/Logs/WorldPartition</code>. Additionally, doing anything that requires streaming generation to run (such as entering PIE or cooking a world partition level) will also create a streaming generation log in that folder to aid in debugging.</p><p>Actors with <code>Is Spatially Loaded</code> disabled and which have no runtime data layers assigned will be saved into the outer level directly, just like an actor in a legacy sublevel-based world. This guarantees that these actors are loaded at all times and simplifies their loading process.</p><p>Non-spatially-loaded actors on runtime data layers will still get extracted to separate streaming levels and will no longer provide any guarantees about loading behavior since they stream in like everything else.</p><p><strong>Runtime Cell Transformers </strong>are used to apply additional transformations to the generation of streaming levels. These are classes with simple &quot;(Pre/Post)Transform&quot; methods that can be added to world settings - every streaming level generated is fed into the transform methods and the transformer class can opt to modify the level or run any other operation you want. A &quot;simple&quot; example is provided in the engine in the form of an ISM transformer that finds all actors using the same mesh in the same streaming cell, and then replaces them with a single ISM component. The <code>FastGeometryStreaming</code> plugin also uses a transformer in order to replace actors with an optimized representation.</p><div class="kg-card kg-callout-card kg-callout-card-red"><div class="kg-callout-emoji">&#x26A0;&#xFE0F;</div><div class="kg-callout-text">The persistent level containing all non-spatially-loaded actors will <i><em class="italic" style="white-space: pre-wrap;">NOT</em></i> be fed to stack of runtime cell transformers as it is not a streaming cell.</div></div><div class="kg-card kg-callout-card kg-callout-card-red"><div class="kg-callout-emoji">&#x26A0;&#xFE0F;</div><div class="kg-callout-text">Despite the name, <i><em class="italic" style="white-space: pre-wrap;">runtime </em></i>cell transformers do not operate at runtime: they run only when streaming generation runs in the editor (entering PIE, cooking, etc). They modify generated runtime cells, hence the name.</div></div><p></p><h3 id="more-about-external-data-layers">More about External Data Layers</h3><p>Actors can be placed into a single EDL at a time but may be placed into other non-external-data-layers.</p><p>EDLs must be registered through the use of a game feature action (or custom code calling into the EDL subsystem) before they can be used. Actors placed into an EDL will be stored into the content directory of the plugin that registered the EDL as noted previously, and thus the generates streaming cells for the EDL will also be stored as part of that plugin to be injected at runtime when the EDL is registered.</p><p>EDLs work by creating a new container for each EDL on a map, with the container&apos;s path pointed at the content directory of the plugin that registered the EDL. The result is that streaming generation places any generated levels inside the plugin that owns the EDL instead of whatever owns the outer world partition level.</p><h4 id="external-streaming-objects">External Streaming Objects</h4><p>The output of streaming generation for an EDL, aside from the streaming level packages, is also an <strong>external streaming object</strong> which stores metadata about each runtime streaming cell and the streaming levels that were created for them. At runtime, external streaming objects are injected into a world partition and are what allow the EDL&apos;s content to be loaded by the outer world partition.</p><h3 id="level-instance-limitations">Level Instance Limitations</h3><p>As mentioned earlier, level instances are not compatible with EDLs. They can&apos;t be placed inside EDLs, and they can&apos;t contain EDLs (well they can, but those EDLs won&apos;t work). Also, content bundles have the same limitation and won&apos;t work either. This is a pretty massive problem if you need to ship any kind of live service/scheduled content on a common level over time. I&apos;m not sure how Epic avoided implementing these features - my understanding is that they have pretty heavy uses of both level instances and EDLs/content bundles, though I think I&apos;ve heard that the Fortnite branch of the engine has a heavily customized variant of the content bundle system that maybe addresses the issue.</p><p>Anyway, I&apos;ve managed to fix both of these issues though the changes are fairly complex and deep in world partition. I can&apos;t post the full set of changes, but I can briefly describe how it works. This is <em>NOT</em> for the faint of heart - it involves a large number of changes to low level parts of world partition, and this is meant to be more of an explanation of how it works in theory rather than instructional on how to do it yourself.</p><p>First, a primer on how EDLs work. When you add an EDL to a level it first spawns a new <code>AWorldDataLayers</code> actor. When you make a world partition level you always get one of these actors by default and it is used to store and configure your list of data layers. This extra EDL actor is used to configure the EDL itself along with any sub-layers, and is stored within the EDL&apos;s external content path.</p><p>When an EDL is injected into a world, the data layer manager (<code>UDataLayerManager</code>) searches the EDL&apos;s content path for this actor (via its actor descriptor) and then attaches a container instance pointing to the rest of the EDL&apos;s content for that level. This results in the EDL&apos;s contained actors being made known to the level&apos;s world partition, and also segments the content so it can be cooked to the EDL&apos;s parent plugin instead of the level it was placed in.</p><pre><code>(Container Instance) Root
|- AWorldDataLayers (default instance)
|- AWorldDataLayers: EDL_MyLayer
|-- (Container Instance) EDL_MyLayer
</code></pre><p>The first problem with level instances - not being able to place them inside EDLs - is I believe just a small oversight. When the data layer manager creates a container instance for an EDL, it does <em>not</em> ask world partition to create the full hierarchy of containers below that instance (done by setting <code>UActorDescContainerInstance::FInitializeParams::bCreateContainerInstanceHierarchy</code> to true when creating the container instance). I believe there are some other small checks/ensures that need to be removed or fixed up as a result of this change, but otherwise this is the fix.</p><p>The second problem is much more complicated to fix. It stems from two rules around how the hierarchy of container instances works:</p><ol><li>All container instances aside from the root must have a &quot;parent&quot; actor descriptor (this descriptor doesn&apos;t necessarily have to create the container instance, but the container instance must belong to the actor).</li><li>A single actor can have at most one child container instance.</li></ol><p>So a level instance actor descriptor creates a single child container instance that contains the referenced level&apos;s actor instance descriptors. But that level instance actor could not, for example, spawn a second level by itself since it would not be able to create another container instance.</p><p>Let&apos;s illustrate a small hierarchy of containers:</p><pre><code>(Container Instance) Root
|- ALevelInstanceActor: MyLevel
|-- (Container Instance) MyLevel
</code></pre><p>Here&apos;s a simple hierarchy showing the root container, a level instance actor, and the container instance it created. This is all allowed. Now, if we wanted to have an EDL <em>inside</em> the level instance, we&apos;d need to add a container to the level instance actor to store any actors inside that level that are in the EDL.</p><pre><code>(Container Instance) Root
|- ALevelInstanceActor: MyLevel
|-- (Container Instance) MyLevel
|-- (Container Instance) EDL_MyLayer &lt;--
</code></pre><p>Except this isn&apos;t allowed - now there are two container instances parented to a single actor!</p><p>The solution is to try to mimic what the external data layer manager already does - use the EDL&apos;s <code>AWorldDataLayers</code> as the parent of the container. Something like this:</p><pre><code>(Container Instance) Root
|- ALevelInstanceActor: MyLevel
|-- (Container Instance) MyLevel
|--- AWorldDataLayers: EDL_MyLayer &lt;--
|---- (Container Instance) EDL_MyLayer
</code></pre><p>Doing so is a bit complicated - the data layer manager needs to be updated to listen for any container instance being registered (via <code>UWorldPartition::OnActorDescContainerInstanceRegistered</code>), then iterate through injected EDLs and run asset registry searches to find an actor descriptor for the EDL&apos;s <code>AWorldDataLayers</code> for that container&apos;s level. If one doesn&apos;t exist, then ignore it. If one does, add the actor descriptor to the incoming container instance and then add a container instance pointing at the EDL&apos;s content to that <code>AWorldDataLayers</code> actor descriptor.</p><p>That&apos;s the gist, but there are also a whole list of other problems to fix that are all complex in their own right too:</p><ol><li>EDLs won&apos;t act properly in the data layer outliner if under a level instance. This is due to a level instance&apos;s external data layer manager not injecting data layers for that level - the fix is to let the manager inject EDLs for the level instance itself. Note that the EDL manager for level instances should <em>not</em> listen for further container instances being registered to inject EDL container instances into - only the outermost manager should handle that process to avoid multiple trying to register at once.</li><li>Sequencer bindings will break if bindings reference actors in EDLs inside level instances. This can be fixed by adding a new container id to <code>FWorldPartitionResolveData</code> and filling it out in <code>FWorldPartitionLevelHelper::LoadActorsInternal</code> - this can then be used from within <code>FActorLocatorFragment::Resolve</code> (via <code>FActorLocatorFragmentResolveParameter</code> which also needs a new field to store the EDL container) to calculate the proper path to the actor. This also requires plumbing through the parent container id as part of <code>FWorldPartitionRuntimeCellObjectMapping</code> to set the resolve data properly in the first place.</li><li>Soft references into/out of EDLs will end up mangled and not resolve properly. Similar to the sequencer issue but for any soft references rather than just sequencer bindings. <code>FWorldPartitionRuntimeContainerResolver::BuildContainerIDToEditorPathMap</code> must be updated to calculate the proper editor path if the given container instance is an instanced EDL. This requires plumbing through whether a container is an instanced EDL as part of <code>FWorldPartitionRuntimeContainerInstance</code>, which can have its value set in <code>FWorldPartitionStreamingGenerator::CreateContainerResolver</code></li></ol><p>If somehow you manage to do all of the above, you&apos;ll have EDLs working inside of level instances and level instances working inside of EDLs.</p><h1 id="the-end">The End</h1><p>Bye!</p>]]></content:encoded></item><item><title><![CDATA[Been Busy]]></title><description><![CDATA[<p>Despite my desires to continue building out the various projects I work on in my free time, the reality is that I simply don&apos;t have the desire or drive to do so these days.</p><p>That sounds like the lead-up to a post about depression (something I certainly have</p>]]></description><link>https://xbloom.io/2023/12/24/been-busy/</link><guid isPermaLink="false">6587b610bdf58518fe4e1a37</guid><category><![CDATA[self]]></category><dc:creator><![CDATA[Sam Bloomberg]]></dc:creator><pubDate>Sun, 24 Dec 2023 04:45:39 GMT</pubDate><content:encoded><![CDATA[<p>Despite my desires to continue building out the various projects I work on in my free time, the reality is that I simply don&apos;t have the desire or drive to do so these days.</p><p>That sounds like the lead-up to a post about depression (something I certainly have struggled with in the past), but ironically what has brought this on is the opposite. My current work is fulfilling in all of the ways that my side projects had been previously. The result is not necessarily that I&apos;m spending more time working (though I certainly do at times compared to my previous position) but that I simply don&apos;t feel the need to do that kind of work in my own time anymore.</p><p>That said, this is certainly a cycle. Nothing lasts forever, and I expect I&apos;ll be back on the side-project-train one day. I&apos;ll probably work on little things here and there even before then - I&apos;m certainly active enough in various gamedev communities to pick up ideas for little things to go after - but it&apos;s unlikely I&apos;ll be attempting anything too big (or finishing up my current projects) in the near term.</p>]]></content:encoded></item><item><title><![CDATA["Visual Horror": coming soon...]]></title><description><![CDATA[<p>This will be a very short experimental game. I originally built it to prototype an idea for a &quot;monster&quot; in a horror game that would try to get you to look at it, as it would only attack if it was visible.</p><p>That turned into some puzzles that</p>]]></description><link>https://xbloom.io/2023/02/24/visual-horror-now-on-itch-io/</link><guid isPermaLink="false">63f8f871ee8149b43ca92fde</guid><category><![CDATA[project: visual horror]]></category><category><![CDATA[unreal engine]]></category><dc:creator><![CDATA[Sam Bloomberg]]></dc:creator><pubDate>Fri, 24 Feb 2023 17:50:57 GMT</pubDate><media:content url="https://xbloom.io/content/images/2023/02/TitleDisplay_Cropped-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://xbloom.io/content/images/2023/02/TitleDisplay_Cropped-1.png" alt="&quot;Visual Horror&quot;: coming soon..."><p>This will be a very short experimental game. I originally built it to prototype an idea for a &quot;monster&quot; in a horror game that would try to get you to look at it, as it would only attack if it was visible.</p><p>That turned into some puzzles that could be built on the same concept - in some ways similar to the opening segments of antichamber, but with more of a horror feel. I never planned on actually building this idea out, but ended up having a lot of fun coming up with a few short levels.</p><p>The game is basically done - it has a purposefully limited scope and makes use only of my very limited artistic ability. Now I mostly need to playtest and refine a few things, and deal with some performance issues.</p><p>In particular, the &quot;vision&quot; mechanic is dependent on receiving data back from the renderer to know whether certain objects are visible. This information is inherently always at least a frame behind the current tick due to how rendering works.</p><p>Which means I need as high a frame rate as possible... And unfortunately the &quot;vision&quot; mechanic is also the worst offender in terms of perf due to using a scene capture, causing an extra unnecessary render of the scene each frame.</p><p>So... Time to learn the ins and outs of unreal&apos;s renderer so I can move this work into an existing render pass.</p><p>Check it out on <a href="https://missivearts.itch.io/visual-horror">itch.io</a>!</p>]]></content:encoded></item><item><title><![CDATA["Visual Horror" #2]]></title><description><![CDATA[A few little teasers for what I'm working on]]></description><link>https://xbloom.io/2023/02/10/visual-horror-2/</link><guid isPermaLink="false">63e68c39ee8149b43ca92fb8</guid><category><![CDATA[project: visual horror]]></category><category><![CDATA[unreal engine]]></category><dc:creator><![CDATA[Sam Bloomberg]]></dc:creator><pubDate>Fri, 10 Feb 2023 18:27:25 GMT</pubDate><content:encoded><![CDATA[<p>A few little teasers for what I&apos;m working on.</p><figure class="kg-card kg-video-card"><div class="kg-video-container"><video src="https://xbloom.io/content/media/2023/02/TooBright.mp4" poster="https://img.spacergif.org/v1/1920x1080/0a/spacer.png" width="1920" height="1080" loop autoplay muted playsinline preload="metadata" style="background: transparent url(&apos;https://xbloom.io/content/images/2023/02/media-thumbnail-ember90.jpg&apos;) 50% 50% / cover no-repeat;"></video><div class="kg-video-overlay"><button class="kg-video-large-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button></div><div class="kg-video-player-container kg-video-hide"><div class="kg-video-player"><button class="kg-video-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button><button class="kg-video-pause-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/><rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/></svg></button><span class="kg-video-current-time">0:00</span><div class="kg-video-time">/<span class="kg-video-duration"></span></div><input type="range" class="kg-video-seek-slider" max="100" value="0"><button class="kg-video-playback-rate">1&#xD7;</button><button class="kg-video-unmute-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/></svg></button><button class="kg-video-mute-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/></svg></button><input type="range" class="kg-video-volume-slider" max="100" value="100"></div></div></div></figure><figure class="kg-card kg-image-card"><img src="https://xbloom.io/content/images/2023/02/d095b35fba503fa8.png" class="kg-image" alt loading="lazy" width="1920" height="1080" srcset="https://xbloom.io/content/images/size/w600/2023/02/d095b35fba503fa8.png 600w, https://xbloom.io/content/images/size/w1000/2023/02/d095b35fba503fa8.png 1000w, https://xbloom.io/content/images/size/w1600/2023/02/d095b35fba503fa8.png 1600w, https://xbloom.io/content/images/2023/02/d095b35fba503fa8.png 1920w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-video-card"><div class="kg-video-container"><video src="https://xbloom.io/content/media/2023/02/EnterWelcome.mp4" poster="https://img.spacergif.org/v1/1920x1080/0a/spacer.png" width="1920" height="1080" loop autoplay muted playsinline preload="metadata" style="background: transparent url(&apos;https://xbloom.io/content/images/2023/02/media-thumbnail-ember117.jpg&apos;) 50% 50% / cover no-repeat;"></video><div class="kg-video-overlay"><button class="kg-video-large-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button></div><div class="kg-video-player-container kg-video-hide"><div class="kg-video-player"><button class="kg-video-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button><button class="kg-video-pause-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/><rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/></svg></button><span class="kg-video-current-time">0:00</span><div class="kg-video-time">/<span class="kg-video-duration"></span></div><input type="range" class="kg-video-seek-slider" max="100" value="0"><button class="kg-video-playback-rate">1&#xD7;</button><button class="kg-video-unmute-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/></svg></button><button class="kg-video-mute-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/></svg></button><input type="range" class="kg-video-volume-slider" max="100" value="100"></div></div></div></figure>]]></content:encoded></item><item><title><![CDATA["Visual Horror" Prototype]]></title><description><![CDATA[<p>I&apos;ve been working on a prototype for a horror game, based on the idea that you aren&apos;t allowed to look at the &quot;monster&quot;. Some technical and design info after the video.</p><p><strong>CW for a jumpscare/loud noise at the very end of the video</strong></p>]]></description><link>https://xbloom.io/2023/02/02/visual-horror-prototype/</link><guid isPermaLink="false">63db0a44ee8149b43ca92f97</guid><category><![CDATA[project: visual horror]]></category><category><![CDATA[unreal engine]]></category><dc:creator><![CDATA[Sam Bloomberg]]></dc:creator><pubDate>Thu, 02 Feb 2023 01:00:14 GMT</pubDate><content:encoded><![CDATA[<p>I&apos;ve been working on a prototype for a horror game, based on the idea that you aren&apos;t allowed to look at the &quot;monster&quot;. Some technical and design info after the video.</p><p><strong>CW for a jumpscare/loud noise at the very end of the video</strong></p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/P6B1TjKgJ2E?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen title="&quot;Visual Horror&quot; Prototype 1/31/2022"></iframe></figure><hr><p>The original idea just started with the monster killing you when you looked at it, which seemed to me like an interesting design for a horror game since it means you have to listen carefully for where the monster is in order to avoid looking in its direction. To that end, I&apos;ve been messing with steam audio/phonon a bit for accurate spatialization but I still have a lot of work to do on that.</p><p>It also means the AI can have a somewhat unusual design, where its goal isn&apos;t to find the player but instead to stand in places that the player is likely to look towards. Right now the AI is fairly simple - it chooses to walk to a spot either in front of the player, behind the player, or near an active objective (the buttons in the video and certain &quot;screens&quot; that display important information).</p><p>After implementing the basic enemy AI I realized that there could be some interesting uses for visibility checks in puzzles. This resulted in the many &quot;displays&quot; that change depending on whether you&apos;re looking at them, and the glass material that blocks visibility checks. I&apos;ve got a few (currently unimplemented) ideas of how to use these for more puzzle designs beyond the simple &quot;button code&quot; example above so stay tuned :)</p><p>On the technical side, knowing whether the player is looking at a particular actor is done with the use of custom depth/stencil in order to give near-pixel-perfect accuracy for whether the player can see something. I&apos;ve created a subsystem that is assigned a certain range of stencil values, and the subsystem assigns out those values every frame to actors that want to know whether the player is looking at them.</p><p>When there are too many actors to assign each one a unique value, the subsystem instead cycles through the list of actors changing which ones are being checked for visibility every frame. Additionally, the list of actors to check is culled by a number of functions including some basic broad visibility checks (don&apos;t check actors that are behind the player, don&apos;t check actors that are far away).</p><p>Here&apos;s what it looks like when I severely limit the number of values available for checking visibility and slow down the process (no jumpscares this time, just showing what visibility checks look like):</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/HbCGiZpvowQ?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen title="&quot;Visual Horror&quot; visibility breakdown"></iframe></figure><p>It&apos;s practically impossible to tell by eye, but each of the &quot;squares&quot; on the render target have a slightly different value, which allows the system to know exactly which ones are on screen.</p><p>All that said, the visibility system is somewhat inefficient at the moment. It depends on a scene capture with post processing material that writes custom depth to a render target, and then reading that render target back on the CPU to dispatch visibility information to the relevant actors. This means an extra render of the scene which is definitely not necessary - ideally I&apos;d be running a shader during the regular render pass and output a small buffer with visibility information. But that&apos;s a project for a later time - I&apos;m just happy this works as well as it does.</p><p>Anyway, that&apos;s all for now. Not sure where I&apos;ll go with this (or when...) but I think it&apos;s neat :)</p>]]></content:encoded></item><item><title><![CDATA[The importance of preset saves]]></title><description><![CDATA[<p>When building a game with any sort of progression you need a quick way to jump to various points of said progression. For a multiplayer FPS that might be having the ability to debug-drop weapons. For a 2d platformer that might just involve a level select.</p><p>For some games it</p>]]></description><link>https://xbloom.io/2022/01/22/the-importance-of-preset-saves/</link><guid isPermaLink="false">61e4e27053b611efbb54dd23</guid><category><![CDATA[project: untitled-ue5]]></category><category><![CDATA[unreal engine]]></category><dc:creator><![CDATA[Sam Bloomberg]]></dc:creator><pubDate>Sat, 22 Jan 2022 03:43:17 GMT</pubDate><content:encoded><![CDATA[<p>When building a game with any sort of progression you need a quick way to jump to various points of said progression. For a multiplayer FPS that might be having the ability to debug-drop weapons. For a 2d platformer that might just involve a level select.</p><p>For some games it might just involve this wonderful option in the right-click menu of the Unreal Editor:</p><figure class="kg-card kg-image-card"><img src="https://xbloom.io/content/images/2022/01/image-7.png" class="kg-image" alt loading="lazy" width="346" height="54"></figure><p>For many games, the one I&apos;m working on in my spare time included, this isn&apos;t quite enough though. In a game where a single level can have multiple states of progression, you need a way to set those states easily.</p><figure class="kg-card kg-image-card"><img src="https://xbloom.io/content/images/2022/01/image-5.png" class="kg-image" alt loading="lazy" width="445" height="430"></figure><p>This is a panel I built with a list of &quot;preset saves&quot; which are really simple blueprints that setup a save data object however I want. The highlighted section at the top is the currently loaded level in the editor - I can use this to load into any part of the game but generally if I&apos;m actively working on a level then that&apos;s the one I want to load into. In addition to this panel, I also have a console command that will let me do the same:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://xbloom.io/content/images/2022/01/image-9.png" class="kg-image" alt loading="lazy" width="910" height="130" srcset="https://xbloom.io/content/images/size/w600/2022/01/image-9.png 600w, https://xbloom.io/content/images/2022/01/image-9.png 910w" sizes="(min-width: 720px) 720px"><figcaption>Had to blur out the command itself unfortunately. Also, if you&apos;re looking for how to make this sort of fancy auto-complete for your commands <a href="https://twitter.com/siliex01/status/1482932764668096512">check out my tweet on the subject</a> :)</figcaption></figure><p>Clicking on any of the options here will run the specified blueprint on the active save file and then reload the save.</p><p>Most blueprints simply set the level path, player start name, and a &quot;chapter progression flag&quot; (basically an enum that says how far the player has progressed within a set of levels). Some may set other options like level progression flags (for progression within a single level) or attributes on the player themselves (whether they can use abilities, their health, etc).</p><p>Here&apos;s an example of what a blueprint looks like:</p><figure class="kg-card kg-image-card"><img src="https://xbloom.io/content/images/2022/01/image-6.png" class="kg-image" alt loading="lazy" width="1568" height="457" srcset="https://xbloom.io/content/images/size/w600/2022/01/image-6.png 600w, https://xbloom.io/content/images/size/w1000/2022/01/image-6.png 1000w, https://xbloom.io/content/images/2022/01/image-6.png 1568w" sizes="(min-width: 720px) 720px"></figure><p>This one sets the current level and player start (both in the panel on the right), and then the chapter&apos;s progression flag and a level progression flag (the node graph on the left).</p><p>I cannot overstate how incredibly useful this functionality is. To jump to the middle of a level it&apos;s just a click of a button. There are certain areas of levels that would otherwise be impossible to jump to without playing up to that point. A major example is when there are sublevels that are streamed in only after certain events are triggered - I would have to play up until the point those events happen if it wasn&apos;t for these preset saves.</p><h2 id="why-not-json">Why not json?</h2><p><a href="https://twitter.com/siliex01/status/1483171421811458048">I previously posted a snippet showing how to import and export saves as json.</a> This seems like a good way to store a bunch of preset save files that are easily editable right? Well, yes, but there are some benefits to sticking with blueprints here - it&apos;s easier to update references to assets that have moved and there&apos;s extra programmability if a bunch of saves start from a common base. When the game is actively under development and the save format is changing constantly, it&apos;s much easier to keep blueprints up to date instead of json files.</p><p>I do still use json-based saves whenever I need to debug how saves are created or to make a quick edit so there&apos;s still value for me. It just isn&apos;t <em>quite</em> as useful as something blueprint-based.</p>]]></content:encoded></item><item><title><![CDATA[Progress: 12/29/2021]]></title><description><![CDATA[Another short video of a work-in-progress level.]]></description><link>https://xbloom.io/2021/12/30/progress/</link><guid isPermaLink="false">61cea60153b611efbb54dd0c</guid><category><![CDATA[project: untitled-ue5]]></category><category><![CDATA[unreal engine]]></category><dc:creator><![CDATA[Sam Bloomberg]]></dc:creator><pubDate>Thu, 30 Dec 2021 06:42:00 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/PUXMOelm67Y?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure><p>More information about this video can be found in this twitter thread:</p><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Another level that was built months ago. Recently redid the greyboxing for it and added placeholder materials. <a href="https://t.co/0iLVR17mIz">https://t.co/0iLVR17mIz</a> <a href="https://twitter.com/hashtag/UnrealEngine?src=hash&amp;ref_src=twsrc%5Etfw">#UnrealEngine</a></p>&#x2014; Sam Bloomberg (@siliex01) <a href="https://twitter.com/siliex01/status/1476454456405336065?ref_src=twsrc%5Etfw">December 30, 2021</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure>]]></content:encoded></item><item><title><![CDATA[Progress: 12/13/2021]]></title><description><![CDATA[Showing off progress on a small passion project I've been working on. There's a video!]]></description><link>https://xbloom.io/2021/12/14/progress-12-13-2021/</link><guid isPermaLink="false">61b81c7c53b611efbb54dc96</guid><category><![CDATA[project: untitled-ue5]]></category><category><![CDATA[unreal engine]]></category><dc:creator><![CDATA[Sam Bloomberg]]></dc:creator><pubDate>Tue, 14 Dec 2021 04:58:21 GMT</pubDate><content:encoded><![CDATA[<p>This is a project I&apos;ve been working on for quite some time. I&apos;m not an artist, and I don&apos;t have a huge amount of experience in level design, but I&apos;m absolutely a good engineer if I do say so myself. So, I&apos;ve taken my strengths to start building something using whatever existing content I can find to bolster up my weaknesses.</p><p>I&apos;ve put in about a year of spotty work - it&apos;s all done in my spare time and very inconsistently. Most of the work has been setting up systems for later use - my engineering background coming into play. Things like a dialogue system, combat design, level design tools (camera triggers, &quot;object fade&quot; triggers, encounter managers, etc). There are a number of little editor tools that I&apos;ve built to facilitate things as well.</p><p>Almost anything &quot;artistic&quot; aside from a handful of materials, effects, and models was pulled from the Unreal marketplace or other free sources. It&apos;s both impressive and disappointing what you can build with purely an engineering background. I have a budget that one day I&apos;ll be using to commission bespoke work - in the meantime, generic assets is what I&apos;m working with. Being a passion project rather than my career means I have all the time in the world so there&apos;s no rush.</p><p>The video below is two short levels - I actually have a few others that are in various states of blockout, but these are the most &quot;complete&quot;. I&apos;m using that term very lightly - these two levels are still absolutely in the blockout stage, but they are more than literal grey boxes. There&apos;s some level and dialogue scripting in place, some placeholder cinematic sequences, and a whole lot more that I&apos;m likely forgetting to mention. It&apos;s hard to show off something in this state, but I&apos;m proud of what I&apos;ve done so far.</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/AIqNJmaR_uA?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure><p>Additional Credits:</p><ul><li>This work is built on Unreal Engine 5</li><li><a href="https://soundcloud.com/hayden-folker/sets/creative-commons-music">Music (c) Hayden Folker under the CC BY license</a> - I needed some placeholder tracks to test music systems and there&apos;s a lot of good stuff here.</li><li>My good friend <a href="https://twitter.com/nelhex">@NelHex</a> who has been helping me out with some concept art, and drew the only placeholder dialogue portrait that isn&apos;t just a white square.</li></ul>]]></content:encoded></item><item><title><![CDATA[Supertalk has been released]]></title><description><![CDATA[<p>...as an open source project :)</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/redxdev/Supertalk"><div class="kg-bookmark-content"><div class="kg-bookmark-title">redxdev/Supertalk</div><div class="kg-bookmark-description">Dialogue scripting language for Unreal Engine. Contribute to redxdev/Supertalk development by creating an account on GitHub.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.githubassets.com/favicons/favicon.svg"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">redxdev</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/0fa4d12a99434b3f3f57fcb347f3e11e98ea44a0af2983833b09960f8b028c7e/redxdev/Supertalk"></div></a></figure>]]></description><link>https://xbloom.io/2021/06/13/supertalk-has-been-released/</link><guid isPermaLink="false">60c59b861f156cdaeaa72582</guid><category><![CDATA[project: supertalk]]></category><category><![CDATA[unreal engine]]></category><category><![CDATA[cinematics]]></category><dc:creator><![CDATA[Sam Bloomberg]]></dc:creator><pubDate>Sun, 13 Jun 2021 05:46:13 GMT</pubDate><content:encoded><![CDATA[<p>...as an open source project :)</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/redxdev/Supertalk"><div class="kg-bookmark-content"><div class="kg-bookmark-title">redxdev/Supertalk</div><div class="kg-bookmark-description">Dialogue scripting language for Unreal Engine. Contribute to redxdev/Supertalk development by creating an account on GitHub.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.githubassets.com/favicons/favicon.svg"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">redxdev</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/0fa4d12a99434b3f3f57fcb347f3e11e98ea44a0af2983833b09960f8b028c7e/redxdev/Supertalk"></div></a></figure>]]></content:encoded></item><item><title><![CDATA[Unreal "Typewriter" text block effect]]></title><description><![CDATA[<figure class="kg-card kg-image-card"><img src="https://xbloom.io/content/images/2021/06/QY50iAnFHU.gif" class="kg-image" alt loading="lazy" width="1658" height="373"></figure><p>I&apos;ve spent way too long getting this right. It seems so simple to just have a letter appear every fraction of a second in a text block... and it is. At least until you start caring about details like line wrapping not breaking mid-effect and rich text support.</p>]]></description><link>https://xbloom.io/2021/06/07/unreal-typewriter-text-block-effect/</link><guid isPermaLink="false">60bdb4ce1f156cdaeaa72568</guid><category><![CDATA[unreal engine]]></category><category><![CDATA[cinematics]]></category><dc:creator><![CDATA[Sam Bloomberg]]></dc:creator><pubDate>Mon, 07 Jun 2021 06:01:19 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-image-card"><img src="https://xbloom.io/content/images/2021/06/QY50iAnFHU.gif" class="kg-image" alt loading="lazy" width="1658" height="373"></figure><p>I&apos;ve spent way too long getting this right. It seems so simple to just have a letter appear every fraction of a second in a text block... and it is. At least until you start caring about details like line wrapping not breaking mid-effect and rich text support.</p><p><a href="https://github.com/redxdev/UnrealRichTextDialogueBox">I&apos;ve posted the relevant code to get this all working on github.</a> The code will not work out of the box - it relies on some classes that I have not included (related to the dialogue system it was pulled from - they&apos;re <em>very</em> easy to swap out though) and needs some setup in UMG. It also isn&apos;t super optimized and there are some limitations, but it&apos;s something I&apos;ll improve on in the future.</p><p><a href="https://github.com/redxdev/UnrealRichTextDialogueBox/blob/main/README.md">Check out the readme</a> for a more extensive explanation of the problems that this solves, and feel free to file issues on it if you find any problems with it.</p>]]></content:encoded></item><item><title><![CDATA[Realtime turn-based AI]]></title><description><![CDATA[<p>You know how in most forms of media (games, comics, movies, whatever) if a group of people gang up on the &quot;hero&quot; or player, they tend to attack one at a time?</p><p>It&apos;s a <a href="https://tvtropes.org/pmwiki/pmwiki.php/Main/MookChivalry">pretty common trope</a>, but at least as far as games go it&</p>]]></description><link>https://xbloom.io/2021/03/19/designing-a-combat-system/</link><guid isPermaLink="false">5f73950010087d18b00adb68</guid><category><![CDATA[project: untitled-ue5]]></category><category><![CDATA[unreal engine]]></category><category><![CDATA[ai]]></category><dc:creator><![CDATA[Sam Bloomberg]]></dc:creator><pubDate>Fri, 19 Mar 2021 05:00:00 GMT</pubDate><content:encoded><![CDATA[<p>You know how in most forms of media (games, comics, movies, whatever) if a group of people gang up on the &quot;hero&quot; or player, they tend to attack one at a time?</p><p>It&apos;s a <a href="https://tvtropes.org/pmwiki/pmwiki.php/Main/MookChivalry">pretty common trope</a>, but at least as far as games go it&apos;s also somewhat required for the player to have any chance against a large group of enemies.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://xbloom.io/content/images/2020/10/sniTWLPwOm.gif" class="kg-image" alt loading="lazy"><figcaption>A bit chaotic, isn&apos;t it?</figcaption></figure><p>That&apos;s a short gif from a prototype I&apos;m working on. There are eight AI enemies attempting to attack at once - there are slight pauses here and there (each AI is set to take a short break after attacking) but when they decide to attack there&apos;s just too much going on.</p><p>DOOM (2016) uses an interesting method to restrict how chaotic having large numbers of enemies on-screen can be - each AI that wants to attack must request a &quot;token&quot; before it can do so. In this way, the designers can limit how many enemies can be attacking at once by limiting the number of tokens available. To make things even more dynamic, AI can also steal tokens from each other if they believe they have a better chance at attacking.</p><p>I&apos;ve seen this technique simply referred to as using &quot;combat tokens&quot; but I&apos;ve also seen it called having a <a href="http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter28_Beyond_the_Kung-Fu_Circle_A_Flexible_System_for_Managing_NPC_Attacks.pdf">&quot;kung-fu circle&quot;</a> (technically a different method, but it is a similar idea).</p><p>I&apos;m not entirely sure how widespread this technique is overall, but you can see evidence of at least similar techniques in other games. The Batman Arkham series from Rocksteady and WB Montreal very obviously does <em>something</em> to prevent every enemy from attacking at once - usually there are just two or three at most that can attack at the same time. I&apos;m sure there are plenty of other similarly styled fighting games that use such a system as well (not to mention other genres).</p><p>I&apos;ve been taking these ideas and slowly building a similar system for my prototype. It&apos;s fairly simple, basically just a way to force the AI to take turns attacking. But it results in much better behavior:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://xbloom.io/content/images/2020/10/IUQLSGpf2Y.gif" class="kg-image" alt loading="lazy"><figcaption>Same combat encounter as above, but with &quot;combat tokens&quot; turned on.</figcaption></figure><p>Now there are at most three enemies attacking any one time, and usually it&apos;s only one or two. So how does this work?</p><p>On every character that can be targeted by an AI, there&apos;s a &quot;combat token source&quot; component. This component manages handing out tokens to AI that want to attack.</p><p>When an AI asks to attack a character, it tells the token source a <em>value</em> and is given a spot in line in return. This value is set by a designer to represent how &quot;big&quot; an attack is - if it&apos;s something that should be the only attack happening then it should have a high value, if it&apos;s something that&apos;s fine to mix with lots of other attacks then it should have a low value.</p><p>The token source has a maximum value after which it stops letting AI attack. Once that threshold is hit, AI sit in a queue and are notified once enough other AI have finished with their tokens.</p><p>To make sure AIs aren&apos;t just attacking instantly one after the other, there&apos;s also a <em>cooldown</em> associated with token values. Whatever value was taken up by a token is made available over a short period after the token is released.</p><p>Here&apos;s what that looks like:</p><figure class="kg-card kg-image-card"><img src="https://xbloom.io/content/images/2020/10/XIDW62qT65.gif" class="kg-image" alt loading="lazy"></figure><p>If the field names aren&apos;t clear enough, here&apos;s a more thorough description of what&apos;s going on:</p><p><strong>Max Token Value: </strong>The maximum value the token source allows to be used at a time. Each token has a value, active token values added up cannot exceed the max token value.</p><p><strong>Token Cooldown Multiplier: </strong>This is how fast value is made available.</p><p><strong>Active Queue Size: </strong>Token activation happens on a first-come first-serve basis - only the token at the front of the queue will be considered for activation. The problem with this is if it has a particularly large value as the queue may get backed up. To combat this, a designer could tell the token source to instead look further ahead in the queue if the first AI has requested too much value. The active queue size is the number of tokens to look ahead.</p><p><strong>Active Token Value: </strong>This is the total value of all active tokens.</p><p><strong>Cooldown Token Value: </strong>This is the value that has been released from tokens, but hasn&apos;t been made available yet. This counts down at a constant rate (see <em>Token Cooldown Multiplier</em>) until it hits zero.</p><h2 id="conclusion">Conclusion</h2><p>This system is in its early stages and still requires a lot of tuning, just like most of the AI I am prototyping. How well it will work (or how necessary it will end up being) for the style of game I&apos;m creating still has yet to be seen. It was interesting to build, however, and should give some interesting knobs to turn in order to adjust difficulty down the road.</p>]]></content:encoded></item><item><title><![CDATA[Scripted Dialogue Sequences]]></title><description><![CDATA[<p>I&apos;ve been working on a prototype for a game in Unreal as a personal endeavor for around 8-9 months now. One of the systems I setup pretty early on was a way to very easily script cutscenes through blueprint. It&apos;s primarily based around writing textual dialogue,</p>]]></description><link>https://xbloom.io/2021/03/14/scripted-dialogue-sequences/</link><guid isPermaLink="false">604da5b2b04ae21ab7910d40</guid><category><![CDATA[project: untitled-ue5]]></category><category><![CDATA[project: supertalk]]></category><category><![CDATA[unreal engine]]></category><category><![CDATA[cinematics]]></category><dc:creator><![CDATA[Sam Bloomberg]]></dc:creator><pubDate>Sun, 14 Mar 2021 07:08:00 GMT</pubDate><content:encoded><![CDATA[<p>I&apos;ve been working on a prototype for a game in Unreal as a personal endeavor for around 8-9 months now. One of the systems I setup pretty early on was a way to very easily script cutscenes through blueprint. It&apos;s primarily based around writing textual dialogue, and is called &quot;TalkMoment&quot; (the term &quot;Dialogue&quot; is, annoyingly, taken in Unreal and naming is hard, but it gets the idea across).</p><p>Here&apos;s a very short bit of dialogue just to show the basics:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://xbloom.io/content/images/2021/03/nZvUJwKc5H.gif" class="kg-image" alt loading="lazy"><figcaption>Excuse the placeholder, uh, everything.</figcaption></figure><p>This short exchange is driven by a basic blueprint (extending a C++ base class) with actions for character and level interaction (in this case, making the characters face each other) and for displaying dialogue.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://xbloom.io/content/images/2021/03/image.png" class="kg-image" alt loading="lazy" width="1399" height="730" srcset="https://xbloom.io/content/images/size/w600/2021/03/image.png 600w, https://xbloom.io/content/images/size/w1000/2021/03/image.png 1000w, https://xbloom.io/content/images/2021/03/image.png 1399w" sizes="(min-width: 720px) 720px"><figcaption>The larger node at the upper-right is an &quot;expanded&quot; version of the &quot;Speak&quot; node that lets me set additional options like portraits for characters.</figcaption></figure><p>This is very simple, and works very well. It even supports branching dialogue trees:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://xbloom.io/content/images/2021/03/image-2.png" class="kg-image" alt loading="lazy" width="1103" height="628" srcset="https://xbloom.io/content/images/size/w600/2021/03/image-2.png 600w, https://xbloom.io/content/images/size/w1000/2021/03/image-2.png 1000w, https://xbloom.io/content/images/2021/03/image-2.png 1103w" sizes="(min-width: 720px) 720px"><figcaption>Would you rather have cake or pie?</figcaption></figure><p>There&apos;s just one small problem... Take this short exchange:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://xbloom.io/content/images/2021/03/J6xgCJzoCR.gif" class="kg-image" alt loading="lazy" width="785" height="449"><figcaption>Chibi placeholder art courtesy of <a href="https://www.shannonwalsh3d.com/">Shannon Walsh</a>.</figcaption></figure><p>This is just 10 lines of dialogue, with a few extra actions to control timing. If written into a word document it&apos;d take up practically no space on screen, but in blueprint...</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://xbloom.io/content/images/2021/03/image-1.png" class="kg-image" alt loading="lazy" width="362" height="619"><figcaption>This is actually a slightly earlier version of the dialogue above, but the number of actions is pretty much the same length.</figcaption></figure><p>Even on a 27&quot; monitor it is impossible to read anything while keeping this short set of dialogue on screen. And that isn&apos;t even the entire scene - <em>only about a third of it!</em></p><p>Even in dedicated branching dialogue software like <a href="https://www.chatmapper.com/">Chat Mapper</a> or <a href="https://www.articy.com/">articy:draft</a> I&apos;ve always felt like viewing large sets of dialogue is clunky, especially when actually writing said dialogue since you are constantly context-switching to add and move nodes around.</p><p>My solution to this isn&apos;t particularly novel and took only a few days of real work. I had looked at <a href="https://yarnspinner.dev/">Yarn Spinner</a> previously but unfortunately it only has a runtime for Unity. I decided to write my own scripting system inspired by it, but designed with the explicit goal of integrating into my existing &quot;TalkMoment&quot; system and blueprint.</p><pre><code>Player = /Game/Flame/Characters/Player/Sp_Player
Uncle = /Game/Flame/Characters/Tower/Sp_Tower_Uncle

Uncle [Left, Default]: Is something on your mind? {Player}?
Player [Right, Default]: Huh? Oh, did you say something?
Uncle: You&apos;ve got your mind stuck on something. What&apos;s going on?
Player: Just a weird dream. Some people talking and then a loud noise that scared me.
Uncle: ...
Uncle: Are you making mischief even in your dreams now?
Player: Hey!
Player: I mean...
Player: No...
Player: I can&apos;t even really remember what was being said. And I don&apos;t think I could see anything.
&gt; ClearLine</code></pre><p>This is the script powering the same short cutscene as before, but now it takes up a <em>tiny </em>portion of the screen. It allows me to set portraits in <em>[brackets]</em> and let&apos;s me call into both C++ and Blueprint with <em>&gt; commands</em>. I can reference <em>/assets/on/disk </em>which let me share properties between scripts (such as the name of a character and the portraits they use). I can also use standard Unreal <em>{text formatting}</em> to intersperse variables with handwritten text. Since the original system is called &quot;Talk&quot;, I call this &quot;Supertalk&quot;.</p><p>It supports branching dialogue:</p><pre><code>Person1: Foo or bar?
  * Foo
    Person1: You chose foo!
  * Bar
    Person1: You chose bar!
Person2: Cake or pie?
  * Cake
    -&gt; ChooseCake
  * Pie
    -&gt; ChoosePie
    
# ChooseCake

Person2: You chose... poorly.

# ChoosePie

Person2: You chose pie!
Person2: This was the right choice.</code></pre><p>And it can even be used in a mixed blueprint/supertalk environment:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://xbloom.io/content/images/2021/03/image-3.png" class="kg-image" alt loading="lazy" width="1406" height="586" srcset="https://xbloom.io/content/images/size/w600/2021/03/image-3.png 600w, https://xbloom.io/content/images/size/w1000/2021/03/image-3.png 1000w, https://xbloom.io/content/images/2021/03/image-3.png 1406w" sizes="(min-width: 720px) 720px"><figcaption>The highlighted node will play a supertalk script to completion before continuing with blueprint execution. The script can also call back into custom Blueprint events!</figcaption></figure><p>This scripting language is <em>very</em> simple and I want to keep it that way - commands aren&apos;t really handled by the scripting runtime, but instead defer to Unreal to parse arguments and call functions. The &quot;compiler&quot; just takes the text script and serializes the syntax tree as a normal Unreal asset that can then be loaded when the game is running. There&apos;s no such thing as an if statement or any other flow control beyond jumping between sections of dialogue after a choice. All of that is better left to blueprint (or C++) which can handle more complex interactions, while leaving the actual text very simple.</p><h2 id="conclusion">Conclusion</h2><p>I&apos;m slowly going back through any prototype cutscenes I&apos;ve written and finding any large masses of dialogue that can be replaced with Supertalk. It works pretty well so far - I&apos;m sure I&apos;ll keep improving it but the simplicity is nice. The only thing I&apos;d really like to add at this point is some custom syntax highlighting for an editor like VS Code. Maybe some day once I&apos;ve polished things up a bit I&apos;ll release it.</p>]]></content:encoded></item><item><title><![CDATA[Improving Pathfinding + Navigation]]></title><description><![CDATA[<p>In my <a href="https://xbloom.io/2020/01/20/scriptable-cinematics-with-coroutines/">last post</a> I showed off how my cinematic system works. The result was this nice little gif:</p><figure class="kg-card kg-image-card"><img src="https://xbloom.io/content/images/2020/02/v3syK9K7pc-1-.gif" class="kg-image" alt loading="lazy"></figure><p>The caption said, &quot;The navigation system is a little funky and needs a rewrite&quot;... Well, now I&apos;ve done a large refactor of the navigation system and it</p>]]></description><link>https://xbloom.io/2020/02/17/improving-pathfinding-navigation/</link><guid isPermaLink="false">5e49d927feafa406464a78e5</guid><category><![CDATA[project: untitled-monogame]]></category><category><![CDATA[ai]]></category><dc:creator><![CDATA[Sam Bloomberg]]></dc:creator><pubDate>Mon, 17 Feb 2020 01:21:48 GMT</pubDate><content:encoded><![CDATA[<p>In my <a href="https://xbloom.io/2020/01/20/scriptable-cinematics-with-coroutines/">last post</a> I showed off how my cinematic system works. The result was this nice little gif:</p><figure class="kg-card kg-image-card"><img src="https://xbloom.io/content/images/2020/02/v3syK9K7pc-1-.gif" class="kg-image" alt loading="lazy"></figure><p>The caption said, &quot;The navigation system is a little funky and needs a rewrite&quot;... Well, now I&apos;ve done a large refactor of the navigation system and it works <em>much</em> better.</p><h1 id="before-the-refactor">Before the Refactor</h1><p>First, a bit on how things used to work. The pathfinding algorithm used is a very simple implementation of <a href="https://en.wikipedia.org/wiki/A*_search_algorithm">A*</a>. When an agent needs to query the navigation system, the world is broken down into a grid with each grid cell being a size based on the agent&apos;s size. From there, the navigation system can query the world as necessary to see if a space is blocked or not.</p><p>Here&apos;s the same scene as before with the grid visible:</p><figure class="kg-card kg-image-card"><img src="https://xbloom.io/content/images/2020/02/kHqmvcjr4c.gif" class="kg-image" alt loading="lazy"></figure><p>There are four types of cells here:</p><ul><li><strong>Green</strong> cells are part of the final path</li><li><strong>Red</strong> cells are blocked by something</li><li><strong>Yellow </strong>cells were candidates to be chosen for the path (not blocked and in the closed list)</li><li><strong>White</strong> cells weren&apos;t looked at (in the open list)</li></ul><p>There are some fairly obvious issues with the above pathfinding system - the first is that agent movement is fairly jerky - this is because the path following algorithm is very naive (we&apos;ll come back to this later as it isn&apos;t directly related to the navigation system).</p><p>The bigger issue is towards the end of the gif when the two agents are trying to path to each other. In order to facilitate generating paths to moving objects (and to account for dynamic obstacles) the &quot;navigate to&quot; action used by the cinematic system (along with other systems) re-queries the navigation system for a new path every so often. That time is set fairly high by default - 2 seconds. This is because any navigation query happens in full immediately and if there are enough of them in a single frame (or the query is sufficiently large) it can tank performance.</p><p>Before anyone asks - no, the answer was not to make things multi-threaded (navigation relies on the physics engine which is not thread-safe).</p><h1 id="the-new-navigation-system">The New Navigation System</h1><p>First, here&apos;s how things look with the new navigation system:</p><figure class="kg-card kg-image-card"><img src="https://xbloom.io/content/images/2020/02/bP4SeisCQv.gif" class="kg-image" alt loading="lazy"></figure><p>No more going up and left over and over again - the agents walk in (mostly) straight lines (again, we&apos;ll come back to this)! No more weird dancing around when they path to each other!</p><p>Here&apos;s the same scene again with debugging on:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://xbloom.io/content/images/2020/02/7f2DtWZfXb.gif" class="kg-image" alt loading="lazy"><figcaption>The yellow squares have disappeared and there&apos;s now an orange line showing the full path, but otherwise this debug view hasn&apos;t changed.</figcaption></figure><p>Probably the most important change is that navigation is now updating almost every frame. And, while it might not be obvious here, there&apos;s little risk for performance problems. Why? Because pathfinding doesn&apos;t need to complete on the same frame the navigation system is queried!</p><p>Here&apos;s the new flow for how navigation works:</p><ol><li>Agent requests a &quot;token&quot; from the navigation system</li><li>Agent updates token with pathfinding parameters (agent size, start/end, etc)</li><li>(every frame) navigation system loops through the list of active tokens and processes pathfinding for each until it a certain &quot;navigation budget&quot; is reached</li><li>(every frame) agent checks for a new path from the token</li></ol><p>As long as an agent hasn&apos;t cancelled its navigation token the system will continue to update it with new pathfinding results as often as possible. This lets agents react to changes in the environment in real-time without having to worry about overburdening the navigation system each frame.</p><p>As for the &quot;navigation budget&quot;, this can theoretically be anything - I think ideally there&apos;d be a set time constraint where the navigation system can do its thing but right now this is just a hard limit of how many physics queries can happen in a single frame - if a pathfinding result is incomplete when the budget is reached the navigation system pauses and then picks up where it left off next frame.</p><p>There are still some improvements to be made - primarily in the pathfinding algorithm itself. There are a bunch of limitations with the current implementation:</p><ul><li>If an agent is in an open (infinite) space but trying to path somewhere that isn&apos;t possible to reach, it will infinitely expand the pathfinding grid. Currently this is mitigated by limiting how far the pathfinder can search but a better solution would be to path from both sides at the same time - if either side fails to find a path then we know that side is in an enclosed space and a path isn&apos;t possible.</li><li>Incomplete paths aren&apos;t possible - ideally we&apos;d be able to at least have paths that get <em>close</em> to a target even if they can&apos;t reach the target.</li></ul><p>These are problems to tackle in the future - for now I think the system works pretty well.</p><h1 id="better-path-following">Better Path Following</h1><p>The other big piece of this was rewriting how path following works. Originally path following was very naive - it would internally keep track of the next point on the path to head towards and would try to hit that point exactly, even if it ended up overshooting the point at first. Another side-effect was that diagonal movement resulted in near constant horizontal-vertical direction changes.</p><p>My solution was twofold - the first was to swap over to a <a href="https://www.gamedev.net/blogs/entry/2264855-steering-behaviors-seeking-and-arriving/">path following steering behavior</a> which uses a &quot;predicted&quot; position to know where to head towards next and also to allow a bit of leeway rather than strictly adhering to the path.</p><p>The second part of the solution focused on making movement a bit more natural - rather than allowing instant turns I now have agents interpolate between their current movement direction and their desired direction. This is somewhat of a halfway between the instantaneous movement that is used for the player and a more classical steering agent that uses forces to control movement. Here&apos;s what that looks like:</p><figure class="kg-card kg-image-card"><img src="https://xbloom.io/content/images/2020/02/DJmXvOFxuH.gif" class="kg-image" alt loading="lazy"></figure><p>The orange line is the &quot;desired&quot; movement direction as calculated by the path-following behavior. The green line is the &quot;real&quot; movement direction - you&apos;ll notice that the orange line is constantly changing while the green line changes slowly. The algorithm still needs some tweaking but the result it gives is to my eyes pretty good.</p><p>That&apos;s it for now, I&apos;ll leave off with a gif of a bunch of agents all trying to navigate to the player at the same time to attack them:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://xbloom.io/content/images/2020/02/KZFpTG5R3N.gif" class="kg-image" alt loading="lazy"><figcaption>These agents use separate attacking/waiting behaviors via a behavior tree which is why they aren&apos;t all constantly trying to attack.</figcaption></figure>]]></content:encoded></item><item><title><![CDATA[Scriptable Cinematics with Coroutines]]></title><description><![CDATA[<p>I&apos;ve been working on and off on a small 2D ARPG for a year or so. There&apos;s very little to show right now (and it&apos;s almost entirely using placeholder assets) but I&apos;m incredibly proud of some of the underlying tech.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://xbloom.io/content/images/2020/01/image-2.png" class="kg-image" alt loading="lazy"><figcaption><a href="https://kenney.nl/">Kenney</a> makes</figcaption></figure>]]></description><link>https://xbloom.io/2020/01/20/scriptable-cinematics-with-coroutines/</link><guid isPermaLink="false">5e262bf0bc37507bb9e46ef3</guid><category><![CDATA[project: untitled-monogame]]></category><category><![CDATA[cinematics]]></category><dc:creator><![CDATA[Sam Bloomberg]]></dc:creator><pubDate>Mon, 20 Jan 2020 23:41:28 GMT</pubDate><content:encoded><![CDATA[<p>I&apos;ve been working on and off on a small 2D ARPG for a year or so. There&apos;s very little to show right now (and it&apos;s almost entirely using placeholder assets) but I&apos;m incredibly proud of some of the underlying tech.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://xbloom.io/content/images/2020/01/image-2.png" class="kg-image" alt loading="lazy"><figcaption><a href="https://kenney.nl/">Kenney</a> makes some wonderful assets to use for prototyping</figcaption></figure><p>The custom engine is built on top of <a href="http://www.monogame.net/">MonoGame</a> (C#) and includes a fairly comprehensive Lua scripting system (via <a href="http://www.moonsharp.org/">MoonSharp</a>). One of the cool features of Lua is support for <a href="https://www.lua.org/pil/9.1.html">coroutines</a> - a form of cooperative multitasking. You can start a coroutine and that coroutine can pause itself to be resumed later.</p><h2 id="building-cinematics">Building Cinematics</h2><p>One area that&apos;s particularly suited for using coroutines is scripted cinematics. A cinematic script can be a coroutine that yields whenever some action needs to take multiple frames or whenever the script needs to wait for something to complete. Using a coroutine means I can write these scripts completely linearly. Here&apos;s a short example of something that would actually work in my engine:</p><pre><code class="language-lua">-- These are just aliases of existing functions to make things a bit faster to
-- write.
local yield = coroutine.yield
local CA = CinemaActions

return Cinematic(&quot;MyFirstCinematic&quot;, function (data)
	local scene = data.Parameters[&quot;Scene&quot;]
    
    -- Get our &quot;actors&quot; for the cinematic
    local person1 = scene:FindEntityByName(&quot;Person1&quot;)
    local person2 = scene:FindEntityByName(&quot;Person2&quot;)
    
    -- We need to register the fact that we&apos;re using these entities as actors
    -- in our cinematic. Other systems (like player input) can disable or
    -- cancel themselves if they see that an entity is taking part in a
    -- cinematic.
    yield (CA.ActorRegistration{person1, person2})
    
    -- Have person1 walk to person2
    yield (CA.NavigateToEntity(person2, person1))
    
    -- Wait 2 seconds
    yield (2)
    
    -- Have person2 walk to (100, 200)
    yield (CA.NavigateToPosition({100, 200}, person2)
    
    -- Have both people walk to each other in parallel
    yield (CA.Parallel(
    	CA.NavigateToEntity(person2, person1)
        CA.NavigateToEntity(person1, person2)
    ))
    
    -- All done!
    -- Actors automatically get released at the end of the cinematic
end)</code></pre><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://xbloom.io/content/images/2020/01/v3syK9K7pc.gif" class="kg-image" alt loading="lazy"><figcaption>The navigation system is a little funky and needs a rewrite, but this (mostly) works as intended.</figcaption></figure><p>Of course, there&apos;s a lot going on behind the scenes here. There&apos;s a navigation system behind <code>NavigateToEntity</code> and <code>NavigateToPosition</code>, and there&apos;s some scene management going on at the top of the script.</p><p>But the focus here is the use of <code>yield()</code> - every time we call that we return a value to the cinematic system. In some cases that value is an action - like <code>NavigateToEntity</code> - in which case the action will be executed to completion before our coroutine continues. In other cases the action is a number in which case the cinematic waits that long (in seconds) before continuing our coroutine.</p><h2 id="cinematic-actions">Cinematic Actions</h2><p>What&apos;s really cool about this system is that not only can you write cinematics linearly in Lua, but it cleanly integrates with C# as well. C# has support for defining iterators as functions with similar syntax to Lua&apos;s yield:</p><pre><code class="language-csharp">public IEnumerator&lt;int&gt; MyIterator()
{
    yield return 1;
    yield return 2;
    yield return 5;
}

foreach (int value in MyIterator())
{
	Console.WriteLine(value); // Prints 1, then 2, then 5
}</code></pre><p>Sort of similar to Lua coroutines, right? Which means we can implement anything from individual actions in a cinematic to entire scripts in a similar way on the C# side. For example, here&apos;s a trivial implementation of an action that just waits a few seconds before letting further actions continue:</p><pre><code class="language-csharp">public class WaitAction
{
    private float timeLeft;
    
    private WaitAction(float timeToWait)
    {
    	this.timeLeft = timeToWait;
    }
    
    // The private constructor and static function here are just to keep
    // the API as clean as possible - only one way to create this action from
    // both C# and Lua. The [ScriptFunction] attribute is how we register this
    // into the CinemaActions module for Lua.
    [ScriptFunction(&quot;Wait&quot;, Module = CinemaActionsModule.Name)]
    public static CinemaAction Create(float timeToWait)
    {
    	return new WaitAction(timeToWait).Run;
    }
    
    private IEnumerator&lt;object&gt; Run(ICinematicData data)
    {
    	while (this.timeLeft &gt; 0)
        {
            this.timeLeft -= (float)data.GameTime.ElapsedGameTime.TotalSeconds;
            
            // yielding null ends the current frame but continues
            // execution next frame.
            yield return null;
        }
    }
}</code></pre><p>A bit more boilerplate - mostly because this action should be exposed to both Lua and C# - but still pretty simple. Here&apos;s another simple example showing how actions can be nested:</p><figure class="kg-card kg-code-card"><pre><code class="language-csharp">private IEnumerator&lt;object&gt; Run(ICinematicData data)
{
    // Wait 10 seconds
    yield return WaitAction.Create(10);
    
    // do something, idk
    
    // Wait 5 seconds
    yield return WaitAction.Create(5);
}</code></pre><figcaption>&quot;doing something&quot; is generally the best one can hope for</figcaption></figure><p>Of course, nesting actions can be done over and over again, though usually you don&apos;t need to nest things too much.</p><h2 id="backend-implementation">Backend Implementation</h2><p>The implementation of the cinematic system is done on the C# side. The basic idea is that the root cinematic object maintains a &quot;stack&quot; - the active action is always on the top of the stack. Actions can be from C# as <code>IEnumerator&lt;object&gt;</code> or Lua coroutines. At the start of a cinematic the initial action (usually the Lua script itself) is added as the only item on the stack.</p><p>Every frame, the cinematic gets the next result from the active action, running whatever logic is inside it. When the action returns, the return value is examined:</p><ul><li>If the action didn&apos;t return any more values (i.e. we reached the end of the enumerator) then we remove the action from the stack and immediately continue the new topmost action (or end the cinematic if nothing is left)</li><li>If it&apos;s <code>null</code> (or <code>nil</code>, if you&apos;re using Lua) the return value is ignored and the game proceeds to the next frame.</li><li>If the value implements <code>IEnumerator&lt;object&gt;</code> it is placed at the top of the stack to be executed immediately</li><li>If the value is a <code>CinemaAction</code> delegate (<code>delegate IEnumerator&lt;object&gt; CinemaAction(ICinematicData data)</code>) then it is immediately executed and the resulting <code>IEnumerator&lt;object&gt;</code> is added to the stack and executed.</li><li>If the value is a Lua function then the function is added to the stack and immediately executed as a coroutine.</li><li>If the value is a number then a <code>WaitAction</code> is added to the stack and immediately executed. Other &quot;shortcuts&quot; work similarly.</li></ul><p>There are some other minor complications thrown in as well - parallel actions are a bit harder to deal with and don&apos;t work with the stack. Currently the cinematic spins up &quot;sub-cinematics&quot; for each action running in parallel - that way each parallel action gets its own stack.</p><h2 id="closing-words">Closing Words</h2><p>I&apos;ll leave off with one of the more interesting bits of cinematic scripting I&apos;ve done. Recently I&apos;ve been writing scripts for the combat tutorial - and part of the flexible nature of this cinematic system is that I can have the player perform actions while the cinematic continues running in the background, waiting for them to complete a task. So to end this here&apos;s a section of that script that starts the tutorial combat encounter:</p><pre><code class="language-lua">local encounter = scene:FindEntityByName(&quot;Enc_Dummies&quot;):GetComponent(Components.Encounter)

local lowHealth = false

-- This will be called every time the player gets hurt during
-- this part of the tutorial. If the player is about to die,
-- we set a flag and reset their health back to max.
local modifyHit = function (_, e)
    local h = player:GetComponent(Components.Health)
    if e.Damage &gt;= h.Health then
        lowHealth = true
        e.Damage = 0
        h.Health = h.MaxHealth
    end
end

player:GetComponent(Components.DamageReceiver).ModifyDamageEvent.add(modifyHit)

-- Remove the player from the cinematic so player input starts working again
data.Cinematic:DeregisterActor(player)

-- Enables AI for the encounter
encounter:BeginEncounter()
while encounter.Running do
    if lowHealth then
        -- Let the player know they ran out of health (though we reset it in
        -- modifyHit) and then continue the encounter.
        encounter:PauseEncounter()
        yield (CA.ActorRegistration{player})
        yield (CA.Dialogue(dialogue, &quot;swordtut_fail&quot;))
        
        -- Continue the encounter - we&apos;re not resetting anything but the player
        -- health so it should get a bit easier if they&apos;ve gotten any hits on
        -- enemies.
        data.Cinematic:DeregisterActor(player)
        encounter:BeginEncounter()
        lowHealth = false
    else
        -- do nothing until the encounter is over or the player has low health
        yield (nil)
    end
end

-- Remove our modify damage function
player:GetComponent(Components.DamageReceiver).ModifyDamageEvent.remove(modifyHit)</code></pre>]]></content:encoded></item><item><title><![CDATA[Quick demo of a multiplayer top-down shooter]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>I&apos;ve been working on a multiplayer top-down shooter in Unreal 4 recently as a way to learn more about Unreal&apos;s multiplayer systems. I recorded a really quick video to show progress after about 1.5 weeks.</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/Ag-0ywtjT_Q?rel=0" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe><!--kg-card-end: markdown-->]]></description><link>https://xbloom.io/2018/02/21/quick-demo-of-a-top-down-shooter/</link><guid isPermaLink="false">5a8dabb19398190e9f50217e</guid><category><![CDATA[unreal engine]]></category><dc:creator><![CDATA[Sam Bloomberg]]></dc:creator><pubDate>Wed, 21 Feb 2018 17:28:58 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>I&apos;ve been working on a multiplayer top-down shooter in Unreal 4 recently as a way to learn more about Unreal&apos;s multiplayer systems. I recorded a really quick video to show progress after about 1.5 weeks.</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/Ag-0ywtjT_Q?rel=0" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe><!--kg-card-end: markdown-->]]></content:encoded></item></channel></rss>