<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tiny</title>
    <description>The latest articles on DEV Community by Tiny (@tinygamedev).</description>
    <link>https://dev.to/tinygamedev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1066778%2F50a7b24a-2bf4-4e64-bd86-db6cc02ba411.jpg</url>
      <title>DEV Community: Tiny</title>
      <link>https://dev.to/tinygamedev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tinygamedev"/>
    <language>en</language>
    <item>
      <title>Spawn actors at random location on landscape</title>
      <dc:creator>Tiny</dc:creator>
      <pubDate>Mon, 26 Jun 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/tinygamedev/spawn-actors-at-random-location-on-landscape-37no</link>
      <guid>https://dev.to/tinygamedev/spawn-actors-at-random-location-on-landscape-37no</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;We’re working with Unreal Engine and we have a large open map with a landscape. The landscape is heightmap based and naturally it’s not going to be a flat polygon. How can we spawn actors dynamically on this landscape and place them correctly on the terrain so it doesn’t look horrible? If you’re asking this question, you’ve come to the right place!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--866wcIUW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vs44z1bl2gxco4itmlgm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--866wcIUW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vs44z1bl2gxco4itmlgm.png" alt="We can place our chests on most of the map and have it look good" width="800" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An important assumption we’re making is the terrain being relatively low frequency. If that’s not true for your case and you have a super high resolution landscape grid you may get worse results. Imagine you want to place something like a chest on a piece of land that has peaks and dips smaller than the chest. You may not get the bottom of the chest to align very well with the ground. Maybe the result will be acceptable regardless, but I felt I needed to point this out.&lt;/p&gt;

&lt;p&gt;We will deal with some edge cases and build a system that’s trying its best to pick good spots to place the actors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding a spot
&lt;/h2&gt;

&lt;p&gt;In Magivoid we occasionally spawn chests that are meant to be close to the player, but not too close. When we initiate this spawn we randomize a target location that’s within a min and max distance from the player.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;MinDist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;10000.&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 100m&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;MaxDist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;20000.&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 200m&lt;/span&gt;
&lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="nf"&gt;Direction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FMath&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RandRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;FMath&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RandRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Direction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Normalize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;Direction&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="n"&gt;FMath&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RandRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MinDist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MaxDist&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;ACharacter&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Character&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UGameplayStatics&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GetPlayerCharacter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Character&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetActorLocation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Direction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a potential target location for the chest, we now check to make sure we have suitable terrain there to place the object. We do this with a vertical line trace that checks against WorldStatic geometry in an attempt to find the Z coordinate of the landscape. If the trace fails it means we don’t have terrain here, or otherwise messed up our trace. It’s important to pick a long enough line segment to make sure you’re guaranteed to hit your landscape geometry with it. You’ll have to pick this based on what your landscape looks like. If the trace does fail, we just exit our function and try again next frame. We’re not in a rush to spawn the object at a specific frame and can absorb this time slicing to avoid potential spikes in CPU cost.&lt;/p&gt;

&lt;p&gt;This is a good opportunity to do a second type of location culling based on the normal of the terrain. Below we check to see if the line trace hit normal is larger than 40 degrees, in which case we also abort. We don’t want to place a chest on a very steep slope. Presumably it would slide off if we pretended physics worked like in real life. This may or may not be important to you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BGn6MFpd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e38ngll5nmroq5w529rk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BGn6MFpd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e38ngll5nmroq5w529rk.png" alt="We skip locations that are too steep" width="800" height="565"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;FCollisionQueryParams&lt;/span&gt; &lt;span class="n"&gt;QueryParams&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;QueryParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddIgnoredActors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PCGActors&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;FCollisionObjectQueryParams&lt;/span&gt; &lt;span class="n"&gt;ObjectQueryParams&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;ObjectQueryParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddObjectTypesToQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ECC_WorldStatic&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;FHitResult&lt;/span&gt; &lt;span class="n"&gt;Hit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;TraceStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Location&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;FVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;TraceEnd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Location&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;FVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;UWorld&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;World&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GetWorld&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;World&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;LineTraceSingleByObjectType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Hit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TraceStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TraceEnd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ObjectQueryParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;QueryParams&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;UE_LOG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogTemp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to find terrain for chest location"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;TerrainAngle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FMath&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RadiansToDegrees&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FMath&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Acos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FVector&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;DotProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ImpactNormal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))));&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TerrainAngle&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;40.&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;UE_LOG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogTemp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Terrain too steep for chest location"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;Location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ImpactPoint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have now updated our target location with the Z coordinate of the landscape where we might want to place the chest. Let’s go ahead and spawn it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spawning the chest
&lt;/h2&gt;

&lt;p&gt;We have a potentially valid location to spawn a chest at and can now continue with placing our actor. But first, I’d like to make the small addition of randomizing the rotation along the Z-axis. The chest can end up more or less anywhere on the map, we might as well have random rotations for it too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;FRotator&lt;/span&gt; &lt;span class="n"&gt;Rotation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FRotator&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ZeroRotator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Rotation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Yaw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FMath&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RandRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simple yaw rotation is all that’s needed. Let’s spawn the chest next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;FActorSpawnParameters&lt;/span&gt; &lt;span class="n"&gt;SpawnParams&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;SpawnParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SpawnCollisionHandlingOverride&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ESpawnActorCollisionHandlingMethod&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AlwaysSpawn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;AActor&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Chest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GetWorld&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SpawnActor&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AActor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ChestClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Rotation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SpawnParams&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;USkeletalMeshComponent&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;SkelMeshComp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Chest&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetComponentByClass&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;USkeletalMeshComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SkelMeshComp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;UWorld&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;World&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GetWorld&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;LineTrace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FVector&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FVector&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Normal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;FCollisionQueryParams&lt;/span&gt; &lt;span class="n"&gt;QueryParams&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;QueryParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddIgnoredActor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Chest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;FCollisionObjectQueryParams&lt;/span&gt; &lt;span class="n"&gt;ObjectQueryParams&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;ObjectQueryParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddObjectTypesToQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ECC_WorldStatic&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;FHitResult&lt;/span&gt; &lt;span class="n"&gt;Hit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;TraceStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;TraceEnd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Point&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;FVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;TargetLocation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TraceStart&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;World&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;LineTraceSingleByObjectType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Hit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TraceStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TraceEnd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ObjectQueryParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;QueryParams&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ImpactPoint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;Normal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ImpactNormal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="n"&gt;AActor&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Actor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetActor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Actor&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ActorHasTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PCG"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="n"&gt;FTransform&lt;/span&gt; &lt;span class="n"&gt;CToW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SkelMeshComp&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetComponentTransform&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;FBoxSphereBounds&lt;/span&gt; &lt;span class="n"&gt;Bounds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SkelMeshComp&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;CalcBounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FTransform&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Identity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;P0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CToW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransformPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;FVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BoxExtent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BoxExtent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;P1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CToW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransformPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;FVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BoxExtent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BoxExtent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;P2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CToW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransformPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;FVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BoxExtent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BoxExtent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;P3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CToW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransformPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;FVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BoxExtent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BoxExtent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;bTraceSuccess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;T0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;bTraceSuccess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LineTrace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;P0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;bTraceSuccess&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;=&lt;/span&gt; &lt;span class="n"&gt;LineTrace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;P1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;bTraceSuccess&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;=&lt;/span&gt; &lt;span class="n"&gt;LineTrace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;P2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;bTraceSuccess&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;=&lt;/span&gt; &lt;span class="n"&gt;LineTrace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;P3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;bTraceSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;UE_LOG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogTemp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to find chest location, destroying this chest."&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;Chest&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;TotalDot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TotalDot&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;2.7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 3.0 is all normals equal, we leave a bit of wiggle room&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;UE_LOG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogTemp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Terrain normals too dissimilar to place chest, destroying actor: %.2f"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;TotalDot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Chest&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;V0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;T0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;V1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;T0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;V2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;T0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;TN0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;V1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cross&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;V0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;TN0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Normalize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;TN1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;V2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cross&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;V1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;TN1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Normalize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;AvgPosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;T1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;T2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;T3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FVector&lt;/span&gt; &lt;span class="n"&gt;AvgNormal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TN0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;TN1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;TotalDot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AvgNormal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;AvgNormal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;AvgNormal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;AvgNormal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TotalDot&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;3.8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 4.0 is all normals equal, we leave a bit of wiggle room&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;UE_LOG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogTemp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Terrain discontinuity found, destroying actor: %.2f"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;TotalDot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Chest&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;FRotator&lt;/span&gt; &lt;span class="n"&gt;NewRotation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FRotationMatrix&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MakeFromZY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AvgNormal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Chest&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetActorRightVector&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="n"&gt;Rotator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;Chest&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetActorLocationAndRotation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AvgPosition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NewRotation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;UE_LOG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogTemp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Display&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Successfully spawned chest!"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ope, that’s a lot of code. Let’s try and go through it and see what’s happening.&lt;/p&gt;

&lt;p&gt;First, we actually spawn the chest at our preliminary location, noting that we may delete the actor if things go wrong. You can probably do all calculations before spawning the actor, but I’m not sure if that’s worth it. Something to profile if needed but this hasn’t been an issue so far.&lt;/p&gt;

&lt;p&gt;Our chest actor has a SkeletalMesh since it’s animated, but this detail isn’t important. The important part once we have our actor is to get the bounds of the visual mesh. We want this to be as accurate as possible to make sure our next calculations give optimal results.&lt;/p&gt;

&lt;p&gt;From the bounds we extract four points (P0-P3) that represent the corners of our mesh. We set the Z coordinate to 100 cm above the origin of the actor and will use this to initiate new line traces down towards the landscape. We also transform these points by the component transform to get the location of the corners in world space.&lt;/p&gt;

&lt;p&gt;From our four corner points, we trace lines down towards the ground with a helper lambda function. This function returns false if we should discard this spawn location entirely (more about this later), as well as the location of the landscape and its normal where the line trace intersected it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kbuOU4va--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ui4igmyif8jgazyi2neu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kbuOU4va--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ui4igmyif8jgazyi2neu.png" alt="We do a line trace for each corner of the chest to learn what the terrain looks like" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next we do a quick and dirty check on all normals to make sure they’re mostly the same. We do this with a dot product and use 2.7 as the threshold for skipping this location. Since we’re adding three dot products, the max value would be 3.0 if all normals were identical. We eyeballed 2.7 as a threshold here to leave a bit of wiggle room. Anything below this we consider too much variation between the normals.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--c39fZLVd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0bzmuu4yncuf3frwrlee.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--c39fZLVd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0bzmuu4yncuf3frwrlee.png" alt="Some cases make it impossible to place the chest nicely and the normals will tell us when this happens" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We now have four points on a piece of landscape that we consider even enough to spawn a chest on. Since our actor origin is at the center of the bottom of the bounding box, we just use the middle from all four corner points as the spawn location.&lt;/p&gt;

&lt;p&gt;It’s important to note these points may not form a plane and we don’t yet know how to orient the chest. To find out what the chest rotation should be we turn the polygon defined by the four corner points into two triangles and independently check their normals. We then use the average of the two normals to orient the chest. This isn’t ideal but there’s not much we can do if there’s a fold at that location.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2ba7yxS_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9aqc8bpmfrcazeiqeyxz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2ba7yxS_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9aqc8bpmfrcazeiqeyxz.png" alt="We split the bottom of the chest plane into two triangle to check their normals" width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this point we do one final check to see if we need to skip this location. Since we have our intended orientation for the chest, we compare this to the normals of the landscape at the four points where the corners of the chest will end up. We do this to make sure there’s no surprising difference between the two. The case that could cause this is when there’s a significant discontinuity in the terrain, for example something like stairs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KuWffLqN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zykaz99ddtzokjndfpdr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KuWffLqN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zykaz99ddtzokjndfpdr.png" alt="Stairs are a particularly difficult case we just don't even attempt to spawn on" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With stairs we would get a very similar normal, if not identical, for all four corners, but the height for each point would be different, causing the chest to lean too much and its center intersecting with the landscape. We keep it simple and just skip these locations, even though in our map we don’t really have many places that could cause this.&lt;/p&gt;

&lt;p&gt;Finally, we create a new rotation from the data we have and update the location and rotation for our already spawned chest actor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PCG thing
&lt;/h2&gt;

&lt;p&gt;You may remember from earlier when we do our first landscape line trace we have this line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;QueryParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddIgnoredActors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PCGActors&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In addition, once we’re ready to check the four corners we have the following early out in our lamba:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;AActor&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Actor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetActor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Actor&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ActorHasTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PCG"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So what’s up with this PCG stuff?&lt;/p&gt;

&lt;p&gt;Our map uses the new PCG tools from UE 5.2 to populate it with trees, bushes, large rocks, etc. The way this works is, there’s a PCG actor in the level which spawns multiple instanced meshes based on the PCG graph. In our case, we have these large trees in the map:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RN3MMxXk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hmjrtj96f6nvjmxuzf3w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RN3MMxXk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hmjrtj96f6nvjmxuzf3w.png" alt="Our pretty large trees" width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We don’t want to mess with these when spawning chests in the sense that we’d like to avoid any computation cost while deciding how to properly align chests to these objects. We keep things simple and just avoid placing chests on top of any static meshes created by the PCG system. With one exception. We’d like to see chests under these large trees.&lt;/p&gt;

&lt;p&gt;In order to make this possible, when we first run our landscape line trace, we ignore any PCG actors to make sure we only find the landscape and its height for a potential chest spawn location. This is the reason for the first line of code mentioned above.&lt;/p&gt;

&lt;p&gt;When we later trace towards the landscape for the chest corners we include the PCG actor but return false in our lambda and fail any attempt that intersects a PCG mesh. The reason this works is because the first line trace is really long to make sure we find our terrain while the corner traces use the height of the terrain with short line segments to avoid hitting any surrounding trees.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--wps1Dqpo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4tydpmv36lc31h4eg5os.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wps1Dqpo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4tydpmv36lc31h4eg5os.png" alt="A chest under our pretty large tree" width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This gives us a system that works well enough to place chests in most locations on our map. When we do fail to find a location, which happens rarely, we try again on the next frame and sooner or later we will get a chest.&lt;/p&gt;

&lt;p&gt;Hope you found this useful, and thanks for reading this far!&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>unrealengine</category>
      <category>programming</category>
    </item>
    <item>
      <title>Magivoid Devlog #5.2 - Engine upgrade and PCG</title>
      <dc:creator>Tiny</dc:creator>
      <pubDate>Wed, 21 Jun 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/tinygamedev/magivoid-devlog-52-engine-upgrade-and-pcg-3j7e</link>
      <guid>https://dev.to/tinygamedev/magivoid-devlog-52-engine-upgrade-and-pcg-3j7e</guid>
      <description>&lt;h2&gt;
  
  
  Engine Upgrade
&lt;/h2&gt;

&lt;p&gt;We’ve been a bit quiet lately but boy have we been busy. There’s tons of new content in the game. Too much for one post, but I’m getting ahead of myself here. Let’s start with the biggest change.&lt;/p&gt;

&lt;p&gt;Since our last devlog Unreal Engine 5.2 has been officially released and we took this chance to update the project. The more exciting addition in this release was the new procedural content generation tools. This brings Houdini-style graph-based tools for procedurally building out your maps in the Unreal Editor. Very exciting for us, since you may remember our map mostly looking like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MsOQufWH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c3z5zhswxgfgvfoqykgm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MsOQufWH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c3z5zhswxgfgvfoqykgm.png" alt="What our map used to look like" width="800" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With a bit of PCG magic, and the awesome work done by Epic on these tools, our map now looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IU_-q9cM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jh1c1zhezlcpacla5hzi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IU_-q9cM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jh1c1zhezlcpacla5hzi.png" alt="What our map looks like now" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s go over how we got here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Procedural content generation in UE5
&lt;/h2&gt;

&lt;p&gt;The trick to getting good results with these PCG tools is to layer multiple ideas on top of each other. Similar in concept to how you would layer multiple octaves of Perlin noise to get a richer result, only here the operations are much more freeform as you’re working with a graph.&lt;/p&gt;

&lt;p&gt;Our setup is pretty simple and starts with a looped spline that defines the area where we want points to be generated. We apply density noise and a filter to create a more sparse distribution of points and use this first pass to pick locations where we place our largest trees. This forest will serve as the core of our map.&lt;/p&gt;

&lt;p&gt;We have at our disposal graph nodes for various transform operations that slightly rotate and scale the meshes to add a subtle variation in how each tree is placed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vMrtmpLN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5mdoly9u31svratn3mra.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vMrtmpLN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5mdoly9u31svratn3mra.png" alt="Nice forest, but too dense" width="800" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This may look ok, but for our needs it’s a bit too dense. We’ll need a bit of open space as well as freedom to move and see through the trees. It’s an easy fix with another layer of density noise and filter to get rid of random points.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Eh6LDEAg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2y15av7n23y8dw5qpb94.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Eh6LDEAg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2y15av7n23y8dw5qpb94.png" alt="Nice forest, not too dense" width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next step is to fill the gaps between the trees. We do this with various bushes and shrubs by generating grids of points around a random selection of spawn points that are slightly biased towards being closer to the center of the map. After applying randomization to location, rotation, and scale for each point in the grids, we get these nice clusters of bushes that are more concentrated the closer they get to the core of the forest.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_fIHz7jt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgtwfg6z2dxnzgz63jlu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_fIHz7jt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgtwfg6z2dxnzgz63jlu.png" alt="Clusters of spawn points for bushes and flowers" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once we filter these points and pick only a dozen or so to spawn meshes at, we end up with a nice distribution of larger vegetation on the map.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DNr9o3-S--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5128dpftq2spu49srdy8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DNr9o3-S--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5128dpftq2spu49srdy8.png" alt="Resulting bushes and flowers" width="800" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We do another pass of filtering the point grids around the large trees and for points closer to the trees we spawn various mushrooms. The idea here is that mushrooms grow closer to large trees where presumably they get more shade.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vlhMVN07--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/78matw89b7kyvsy4zrsq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vlhMVN07--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/78matw89b7kyvsy4zrsq.png" alt="A bunch of shrooms under trees" width="800" height="595"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We have the ability to detect the distance from a potential spawn point to the edge of our PCG spawn area, defined by our looped spline, which can tell us if a point is closer to the center or the edge of the forest. We use this data to spawn various rocks all over the map, but mostly at the periphery of the forest. We don’t want a complete absence of rocks at the core, but ideally we see more of them closer to the edge of the map and next to the mountains.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kO4mJLKp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vmxpddb7dqn3jf9ptvcc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kO4mJLKp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vmxpddb7dqn3jf9ptvcc.png" alt="A bunch of rocks not all close to trees" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Up next we have a river that introduces a bit of blue in this vast sea of green. The river is built with a landscape spline that gently carves into the terrain and a spline mesh for the water plane. Here the PCG tools are of great help with their support for sampling points along a spline. We use the spline sampling node to detect where our river sits in the map and use this data for two operations. First, we cull any of the points we use for trees and bushes that directly intersect the river. Second, we generate a new set of points along the sides of the river and spawn a new type of vegetation like ferns as well as some additional rocks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aaxgVf-0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xc2f37oj1g6qlrazdp3j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aaxgVf-0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xc2f37oj1g6qlrazdp3j.png" alt="Our pretty river that may also contain some rocks" width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In addition to the river, we used splines to set up a few paths through the forest to break up the monotony of seeing only trees. The road spline is a regular actor with a simple spline mesh that projects itself onto the terrain by writing to a Runtime Virtual Texture. This allows us to paint the path with a different dirt material, remove grass or change its color, and affect the PCG spawn point distribution by culling trees and vegetation automatically if they intersect the path.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--HMiEBeIY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aimqxd8kh6dzyzz7x5wj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HMiEBeIY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aimqxd8kh6dzyzz7x5wj.png" alt="It wouldn't be a forest without a path going through it" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;In the name of keeping each devlog relatively short, we stop here having mentioned some of the world building we’ve done since our last post. There’s a significant amount of new content like monsters and weapons playing really well. I’d love to share more about them soon. Hopefully I will have some time for writing in between sprints of development work.&lt;/p&gt;

&lt;p&gt;We’ve also launched a newsletter for those of you who would like a more intimate view of the development and progress on Magivoid. Don’t expect many emails though, we send them out rarely, but promise they will have worthwhile content!&lt;/p&gt;

&lt;p&gt;Subscribe here: &lt;a href="https://tinyletter.com/tinygamedev"&gt;https://tinyletter.com/tinygamedev&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.unrealengine.com/5.2/en-US/procedural-content-generation-overview/"&gt;UE 5.2 PCG Tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=momc4h5J19Y"&gt;Pathways &amp;amp; Roads using RVTs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/playlist?list=PLA03OHAaHgYpo0enf8p-2oEpja3grLOKZ"&gt;Unreal PCG Tutorial&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gamedev</category>
      <category>devlog</category>
    </item>
    <item>
      <title>Magivoid Devlog #4 - Explosions and Ghouls</title>
      <dc:creator>Tiny</dc:creator>
      <pubDate>Tue, 02 May 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/tinygamedev/magivoid-devlog-4-explosions-and-ghouls-14om</link>
      <guid>https://dev.to/tinygamedev/magivoid-devlog-4-explosions-and-ghouls-14om</guid>
      <description>&lt;h2&gt;
  
  
  Fireball progression
&lt;/h2&gt;

&lt;p&gt;With our first weapon being the fireball spell it should feel like a powerful means of destruction yet still be upgradeable to cause increased collateral damage at higher levels. I have recently implemented multiple levels of the spell and it’s already starting to feel like this is going to be fun.&lt;/p&gt;

&lt;p&gt;Before getting into the three levels of the spell I wanted to mention what plans I had for the combat. At the moment it’s a simple “monster health” vs “weapon damage” ordeal with no additional depth to it. The plan, however, is to introduce multiple stats that can be leveled up and give a variety of different character builds. Examples of these stats are luck affecting how the random weapon damage is calculated, monster defense protecting against player damage, player strength increasing it, etc. Different playable characters will also have different base stats to offer an additional level of strategy when selecting what character to play with. We’ll see what I end up implementing in the end, but I’m excited about the potential for fun mechanics here.&lt;/p&gt;

&lt;p&gt;Right, let’s get into it. The first level of the fireball spell is a simple projectile that inflicts some random amount of damage in a predefined range. It takes 2-3 hits to kill a spider, for example, but you quickly level up to destroy these things much faster.&lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;p&gt;The second level of the spell improves the projectile with a damage-over-time effect if the enemy isn’t killed after a successful hit. The fire DOT does additional damage every second for three seconds. I’m thinking in the future this version can be further improved to make the effect last longer or cause enemies to burn each other when they’re close and have the fire spread.&lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;p&gt;The third level is no longer a single enemy damage weapon. When the projectile is destroyed it triggers an explosion that damages everything in a radius. All levels of the spell stack, which means if the explosion doesn’t kill an enemy it will also apply the fire DOT on everything in the radius.&lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;h2&gt;
  
  
  Uh, ghouls
&lt;/h2&gt;

&lt;p&gt;I added the second monster in Magivoid and I’m happy to say it turned out pretty neat. Its behavior is the same as the spider, with the only exception being the new monster moves slightly faster, so it was quick and easy to implement…mostly. Let me introduce you to the ghoul. &lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;p&gt;I spent a couple of days wrestling with the physics body in Unreal Engine trying to get the ragdoll to behave. I built up the body parts from scratch and set up all capsules and constraints manually, which took longer than I would have liked, but the end result works pretty well.&lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;p&gt;This model also comes with a few different cosmetic props which add a little bit of variation. The meshes are set up to automatically get randomized when the monster spawns and at this time they don’t really affect gameplay in any way. It does look cool though and I like that it introduces a bit of variation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5g4XV8Mx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/femrbzb5vmnluef4n1ot.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5g4XV8Mx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/femrbzb5vmnluef4n1ot.png" alt="Some variation in the ghoul society" width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Experience
&lt;/h2&gt;

&lt;p&gt;You might have noticed in the videos above tiny blue crystals being dropped when monsters get destroyed. These give the player experience when picked up and forces you to get close to them to get the reward instead of awarding you the experience when killing the monster.&lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;p&gt;I’ve made an initial version of a full xp curve from level 1 to 99, but obviously this isn’t very useful or set in stone at this time so I won’t share the numbers just yet. I do like its properties though, and I think it will end up feeling quite rewarding while still giving the player a challenge in an attempt to keep them in the flow zone. We’ll have to see what it plays like once we have more content in there and can actually start to balance the game.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;There’s a bit of a game in there now. We have a few monsters, an upgradable weapon, and a means for the player to earn experience and level up. I’d like to build out the entire game flow from start to finish, including work-in-progress UIs next. This should give the sense of a whole game, regardless of how empty and short it is at this time. From that point on it’s just a matter of filling it out with more content, right? Before doing this though, I may take a quick detour and add a few more monsters.&lt;/p&gt;

&lt;p&gt;Until next time, here’s a fun video that people seemed to enjoy on &lt;a href="https://www.reddit.com/r/IndieDev/comments/12ynem4/did_i_go_too_far_with_the_fireball_spell"&gt;r/IndieDev&lt;/a&gt;.&lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.unrealengine.com/marketplace/en-US/product/ghoul-02"&gt;Ghoul&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.unrealengine.com/marketplace/en-US/product/crystals"&gt;Crystals&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.unrealengine.com/marketplace/en-US/product/hqui-progress-bars"&gt;HQUI: Progress Bars&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gamedev</category>
      <category>devlog</category>
    </item>
    <item>
      <title>Magivoid Devlog #3 - Pivot!</title>
      <dc:creator>Tiny</dc:creator>
      <pubDate>Fri, 21 Apr 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/tinygamedev/magivoid-devlog-3-pivot-1ggn</link>
      <guid>https://dev.to/tinygamedev/magivoid-devlog-3-pivot-1ggn</guid>
      <description>&lt;h2&gt;
  
  
  Back to making games
&lt;/h2&gt;

&lt;p&gt;I got a bit side tracked with &lt;a href="https://tinygame.dev/posts/Gaming-news-with-a-twist"&gt;that web project&lt;/a&gt; but now I’m back with good old Unreal Engine. The short break spent coding websites also got me thinking about what Magivoid is supposed to be. Consequently, with this post comes also a pivot.&lt;/p&gt;

&lt;p&gt;Magivoid was initially meant to be some sort of MOBA-like, multiplayer, with two teams battling each other, etc. This was a fun idea and I may continue to explore it, but I want to make it easier for myself to finish this project. You can always extend the game after it’s been released. For this reason, I’ve been inspired to shift gears slightly and make this more of a single player experience. At least to begin with. This should significantly lower the effort needed to test everything and get a solid release candidate built.&lt;/p&gt;

&lt;p&gt;The current direction is as such. You play a timed session where monsters of increasing difficulty rush you with the goal to survive. You start with a weak weapon, say a fireball spell, and as you destroy monsters and stay alive, you get to level up and upgrade both your character through various attributes as well as your weapon. Simple idea and I would say totally doable. If I don’t manage to finish this project, I need to move back to my grandma’s village and learn to take care of the sheep because there’s no future for me in gamedev.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--be4VvyyY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/68f20z80jcyj0p3xknue.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--be4VvyyY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/68f20z80jcyj0p3xknue.jpeg" alt="Everyone likes a powerful protagonist" width="768" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One concept I’ve always wanted to explore is progressing the player from a weak individual to an overpowered character that destroys everything in their path. I seem to appreciate this in books and movies and it’s a concept not implemented enough in games. I have some cool ideas for how to take a lame fireball projectile and gradually upgrade it into a massive super weapon, but I’ll get to this later once I have something to show. There are also plans for different types of weapons with a similar sort of progression. For now, let’s look at the changes that have actually been implemented in the game.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creepy crawlies
&lt;/h2&gt;

&lt;p&gt;In this revised version of Magivoid you start with various small and relatively weak monsters attacking you. As they increase in numbers, you level up and become more awesome. In turn, bigger and scarier monsters start spawning and also gradually increase in numbers. You keep destroying them, level up, and become more awesome. This is the core idea.&lt;/p&gt;

&lt;p&gt;Naturally, we need to start with a monster and I have already added it to the game. Say hello to the first annoying thing that will chase you: the spider.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--b7Zdn-Mk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/begnnaylw047dc9hopi4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--b7Zdn-Mk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/begnnaylw047dc9hopi4.png" alt="Spiders swarming the player" width="800" height="645"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s been an interesting journey creating an animation blueprint, hooking up the AI, and getting this thing to behave more or less the way I want it to. It’s not doing anything advanced, mind you, but there were a bunch of moving parts to keep track of. The extent of the spider’s skills is limited to spawning, chasing the player, and attacking when in range.&lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;p&gt;Oh, it also breaks apart when it’s killed. I thought this was a fun little detail, made possible by the spider being set up with a physics body and making it ragdoll once its health gets down to zero.&lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;p&gt;The spiders themselves do a random amount of damage controlled by a min/max value specified in a data asset. Their health is also a random value, again controlled by a min/max value. This results in the player sometimes needing two and sometimes three fireballs to kill a spider when the game starts. I’ve always liked RPGs that have slightly random values and give a bit of variability, yet still offer a controlled experience for the gameplay to make sense to the player.&lt;/p&gt;

&lt;p&gt;The amount of spiders that spawn is controlled by a float curve starting at roughly 5 spiders and increasing to 20 spiders at minute 2. As new monsters are added, there will be multiple overlapping curves that together create a pool of monsters chasing the player. These are just arbitrary values at this point and will need balancing as new features are implemented. Data tables, curve assets, and the overall Unreal Engine reflection system make it fairly easy and, dare I say, fun to build these kinds of data-driven components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visual feedback
&lt;/h2&gt;

&lt;p&gt;I added health bars that rendered above each enemy, which was a nice little detail, but I found it distracted from the game world. There are many nice marketplace assets to start prototyping with and it was quick and easy to get this added. I figured it’s nice to know the state of each enemy and potentially how easy or hard it is to kill it by gauging how fast the health drops, but at the same time I like the idea of keeping it unknown and suspenseful. With no visual feedback like health bars you have no idea what kind of threat a new monster poses until you finally destroy it. So, I got rid of the health bars.&lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;p&gt;One visual detail that does make damaging monsters rewarding is the recognizable white flash. I think this is inspired by old games where flashing a sprite was &lt;a href="https://retrocomputing.stackexchange.com/questions/12211/why-do-old-games-use-flashing-as-means-of-showing-damage"&gt;cheap&lt;/a&gt; and used as a means to show damage or death of a character. Regardless, the confirmation that a projectile landed correctly is a nice little rewarding detail. Unfortunately on small enemies like this spider it can be a bit hard to notice the white flash due to the fireball explosion particle effect running at the time. Still, I do like it when it is visible and will probably keep it unless I find a better way to show damage being done.&lt;/p&gt;

&lt;p&gt;Let me see if I can bump up the scale of the spider so we see the hit flash in action.&lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;p&gt;Nice!&lt;/p&gt;

&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;With these changes the project is starting to feel a bit like a game. When I debug things I constantly find myself running around killing spiders for just a bit longer than I need to get the job done. I feel it’s possible to extend this to something fun to play.&lt;/p&gt;

&lt;p&gt;In the next devlog I’ll likely cover some of the weapon upgrades and see just how fun and overpowered I can make this fireball attack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.unrealengine.com/marketplace/en-US/product/spider-minion"&gt;Spider minion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.unrealengine.com/marketplace/en-US/product/dynamic-health-bars"&gt;Dynamic Health Bars&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gamedev</category>
      <category>devlog</category>
    </item>
    <item>
      <title>Building gamebiz.news</title>
      <dc:creator>Tiny</dc:creator>
      <pubDate>Mon, 17 Apr 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/tinygamedev/building-gamebiznews-lgh</link>
      <guid>https://dev.to/tinygamedev/building-gamebiznews-lgh</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;I assure you, work on &lt;a href="https://tinygame.dev/tags/magivoid/"&gt;Magivoid&lt;/a&gt; is going great with a new devlog coming online very soon! However, as promised in the &lt;a href="https://tinygame.dev/posts/Gaming-news-with-a-twist/"&gt;previous post&lt;/a&gt; where I introduced &lt;a href="https://gamebiz.news"&gt;gamebiz.news&lt;/a&gt;, here’s some info on the details of how it was built.&lt;/p&gt;

&lt;p&gt;This endeavor has been a slight distraction from making games, but coding is coding and it was great fun building this little web app. I learned all kinds of new web and backend technologies. Who knows, Magivoid or some other game I work on may need a backend at some point and I should be able to build that after this exercise. But enough jibber-jabber, let’s get into the weeds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--u8Orn088--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o1ujxm44dihpm9im6na2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--u8Orn088--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o1ujxm44dihpm9im6na2.png" alt="gamebiz.news front page" width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;I’m going to assume you haven’t seen &lt;a href="https://gamebiz.news"&gt;gamebiz.news&lt;/a&gt; and start with a bit of a description. The app collects news articles from a handful of gaming news websites that I frequent and lists them all in one place. The primary goal was to make it quick and easy to get an idea of what’s new in the gaming world.  I’ve had some requests to extend the site with additional sources but I found most of them don’t offer anything unique in addition to the current coverage.  If you do think something is missing though, please do get in touch!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Jp8zalKp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b93p3p6yng0oot5fiowi.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Jp8zalKp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b93p3p6yng0oot5fiowi.jpeg" alt="A generic AI generated image. Pretty cool, right?" width="768" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Surprisingly, building the basic functionality was relatively fast and easy to do. I kept pushing and implemented some additional features for fun. I added support for up/down voting articles where the score counts towards the author of the article rather than the post itself. The idea is, if the site gets any traction and more people use it, maybe there’s an “author quality” type of metric that comes out of this system.&lt;/p&gt;

&lt;p&gt;For the voting to work well, I needed a way to control who can vote and when. This led me to add an authentication and account system. You don’t need to create an account to read the news on the site, but you do need to sign up if you want to vote. This needs to be the case to ensure people don’t abuse the voting system. Or rather, you can’t abuse it easily, I hope. Please don’t try too hard to hack it, I’m sure there’s a way.&lt;/p&gt;

&lt;p&gt;Additionally, I planned on adding a commenting system for people to start discussions centered around each post. This has no value in the beginning while few people use the site, so I decided to postpone this until there’s more traffic. I’m looking forward to implementing this system though, and I hope&lt;br&gt;
I get a chance to do it soon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend
&lt;/h2&gt;

&lt;p&gt;The frontend of the app, the website itself, is built using&lt;br&gt;
&lt;a href="https://nuxt.com/"&gt;Nuxt3&lt;/a&gt;. Making the transition from gamedev to webdev has been interesting, to put it lightly.  Everything is open source and there’s a bajillion frameworks, which is more or less the opposite of what you’ll find in gamedev where everything is kept secret and your only tool is the engine you&lt;br&gt;
commit to using.&lt;/p&gt;

&lt;p&gt;Nuxt is a framework on top of the different framework, &lt;a href="https://vuejs.org/"&gt;Vue.js&lt;/a&gt;, which for some reason I gravitated towards and have enjoyed using. I briefly tried &lt;a href="https://react.dev/"&gt;React&lt;/a&gt;, arguably the industry standard, but it never really clicked for me like Vue did. In addition to wrapping all of Vue, Nuxt also handles a lot of boilerplate javascript setup for you and makes it easy to build web apps without needing to worry too much about the internals. In many ways it’s similar to using a game engine.&lt;/p&gt;

&lt;p&gt;My favorite thing about Nuxt is how easy it is to create server-side rendered web apps where code runs on the server (as opposed to in the browser). You write all code in the same place and it’s easy to specify what runs on the client and server as needed. The caveat is you do need a server to host your backend if you go this route as it’s not a static website anymore, but nowadays there are many simple options for hosting dynamic sites and this isn’t really a problem. Server-side rendering can be a great benefit when your site has a lot of text. Search engine crawlers get all that content for their indexing, making your site more visible to additional keywords. It usually also means your site loads faster since the final web page is delivered and no (or little) javascript has to run client-side to render the page.&lt;/p&gt;

&lt;p&gt;For the visuals I really like &lt;a href="https://tailwindui.com/"&gt;Tailwind&lt;/a&gt; and how easy it is to make nice looking UIs with it. Some people dislike the inline way you have to set up all your classes, but I’ve found this to be a positive. I’ve always disliked having CSS data in a different location from the HTML element. &lt;/p&gt;



&lt;p&gt;Anyway, I’m not really a webdev and you may want to consider other resources for more legit suggestions if you want to get ideas about what to use yourself. Tailwind has worked great for me and with minimal, if not zero, skills in user interface design I managed to create something that’s not horrible on the eyes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FgsQctmT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z6mo7n48gl09epuxc8f0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FgsQctmT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z6mo7n48gl09epuxc8f0.png" alt="This is what the signup page looks like" width="800" height="142"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication
&lt;/h2&gt;

&lt;p&gt;As mentioned previously, to keep the article voting fair and controlled I needed a way to limit who gets to vote and how. I wasn’t about to write a new authentication system from scratch, which sounds like a complicated system to implement well, especially with all the great solutions out there already.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KjoAC5d5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ko25usow90357xkxbujl.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KjoAC5d5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ko25usow90357xkxbujl.jpeg" alt="Another generic AI generated image, this time related to our auth system" width="768" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I decided to use &lt;a href="https://supabase.com/"&gt;Supabase&lt;/a&gt; for this. In fact, I’m using Supabase for both authentication and the backend database. The database is just a managed Postgres instance with great RLS support connected to the authentication system. This lets me create user-private data in the future, if needed. At the moment RLS is only used to make sure authenticated users are the only ones that can vote on articles and of course limit who can add new articles to a service account used by the backend script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backend script
&lt;/h2&gt;

&lt;p&gt;So far we have a website and a database to store all the data. The only problem is, the website is empty because the database is empty. We need a way to populate our database with the articles from various sources.&lt;/p&gt;

&lt;p&gt;I already had a list of a handful of gaming news sites that I like. The next step was to somehow automate the retrieval of data from these sites to populate the database. I decided to use &lt;a href="https://go.dev/"&gt;Go&lt;/a&gt; for this step. I can’t remember if there was a specific reason for this choice. I just really like Go for quick hacking projects and use any excuse I can to make use of this language, so here we are.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_y6zAM4j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3xs5j2nu5u8wgakheh87.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_y6zAM4j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3xs5j2nu5u8wgakheh87.jpeg" alt="Aaaand, another generic AI generated image, this time related to our backend script, somehow." width="768" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I wrote a simple console app that reads an RSS feed, iterates over all items in the feed, and extracts the relevant data. That relevant data in this case is just the URL of the article, the author, and its publication date. Once the data is parsed from the RSS feed, it adds an entry to our database for each article.&lt;/p&gt;

&lt;p&gt;The database also holds a table with all known authors across all news sites. When the script parses the RSS feeds, it looks at the author of each item and makes sure we have an entry for them in our database. If we don’t, a new author is added so we have something to connect each article to.&lt;/p&gt;

&lt;p&gt;After processing all the RSS feeds, the script does a pass over all authors in the database to calculate their up-to-date score. The score for each author is a combination of the number of articles written by them, the sum of all upvotes, and the sum of all downvotes. This total score is then written back to the database, ready for our frontend to display as needed.&lt;/p&gt;

&lt;p&gt;This script runs as a cron job every four hours and continuously updates our database with new data as new articles are being posted across the internet.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI summaries
&lt;/h2&gt;

&lt;p&gt;I realized that often I tend not to read full articles. Occasionally I read the whole thing, but in most cases I either just skim an article or, worse, read only the headline. Once I had my backend script parsing the RSS feeds of all my favorite sites, I also learned just how many articles are actually published every day. You don’t get a clear impression of this by randomly visiting websites, but when you get every new post printed to a log every four hours you see exactly what the volume is. There is no way I would have time to read all these articles every day.&lt;/p&gt;

&lt;p&gt;I didn’t plan on solving this in any way, but a friend of mine accidentally gave me an idea. He was at the time working on an app of his own using generative AI. While both of us were hacking on our projects we kept discussing random ideas. All that talk about AI spawned the suggestion of auto-creating summaries. After all, AI is meant to be pretty good at this stuff.&lt;/p&gt;

&lt;p&gt;A short google search, a few bounces of the idea between my friend and I, and a few hours later I had AI summaries working and live on the site. It really took only a few hours to go from idea to having it run and work well, and I will tell you what the secret is: 🤗&lt;/p&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/"&gt;Hugging Face&lt;/a&gt; is an amazing AI community where you can find all kinds of information and ready to use solutions. For my site I ended up using a deployment of the &lt;a href="https://huggingface.co/facebook/bart-large-cnn"&gt;facebook/bart-large-cnn&lt;/a&gt; model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Xs7bclK5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nh9ax0gu6vb1f0ts12q2.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Xs7bclK5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nh9ax0gu6vb1f0ts12q2.jpeg" alt="Ok, I promise this is the last AI generated image. I had to! The post had too much text." width="768" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The implementation of AI summaries consisted of some minor frontend UI tweaks to pull the summary text from the database and display it as needed, and a small addition to the backend script. Unfortunately, the RSS feeds for the sites I gather articles from don’t contain the entire article. Some of them have a short summary, but getting an AI to summarize a summary didn’t produce meaningful results. Displaying the summary provided in the RSS feed was also out of the question as some of them had ads and other random links in them that I didn’t want on my site. I needed a different solution.&lt;/p&gt;

&lt;p&gt;The solution ended up being an HTML scraper. We already have a URL for each article we process. It was a small step from there to visit that URL, find where the meaty text of the article was, and extract the entire thing into a string. This then gets passed to our AI model and back comes a sweet summary that was surprisingly well phrased in most, if not all, cases. The summary then gets added to the database along with the other article specific data for the web frontend to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;p&gt;To get the whole party deployed required a few moving parts but it wasn’t as complicated as I initially expected. The web frontend and backend is deployed to &lt;a href="https://vercel.com/"&gt;Vercel&lt;/a&gt;. Nuxt is already set up to very easily deploy to Vercel so this just worked with minimal setup from my end. Each time I commit changes to my main branch in Github, it auto deploys to Vercel and changes go live.&lt;/p&gt;

&lt;p&gt;Gamedevs, imagine every Perforce commit being cooked into a patch that gets auto deployed to your players. Wouldn’t that be quite the future?&lt;/p&gt;

&lt;p&gt;The backend script that gathers article data runs as a cron job on a $5 VPS from &lt;a href="https://www.linode.com/"&gt;Linode&lt;/a&gt;. It’s a very low-cost script that doesn’t require much resources to run. In fact, this is the same VPS that runs my Perforce server for Magivoid.&lt;/p&gt;

&lt;p&gt;For the AI summaries, the model used is running on Hugging Face. There are various options for deploying workloads there, it’s really a great platform. Again, this is also a low-cost requirement as we really just need at most ~30 articles summarized every four hours. If this ever scales to higher numbers we may need more oomph here, but I haven’t noticed any issues so far.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discord bot
&lt;/h2&gt;

&lt;p&gt;Overall this project has relatively few moving parts with not much that can go wrong. Still, we’re dealing with internet connections, multiple websites, APIs, and a bit of automation that would be nice to keep track of. I don’t really want to SSH into the VPS to check logs regularly either, so I came up with this odd gimmick for keeping track of when things go wrong. I know there are various professional solutions out there, but they are all overkill for my needs. All I need, apparently, is a Discord bot that posts warnings and errors to&lt;br&gt;
a channel on my server.&lt;/p&gt;

&lt;p&gt;I’ve had the Discord bot hooked up to the website backend at some point, but decided this wasn’t worth the spam it generated when the site got a bit of traffic. At the moment, the bot only runs within the backend script that gathers the article data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YRghUeYX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5gknbwn44sq3w8hwrd3d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YRghUeYX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5gknbwn44sq3w8hwrd3d.png" alt="An example of what our Discord bot posts as an update" width="800" height="213"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every four hours I get posts to a private Discord channel telling me how many new articles have been processed and added to the database, how many new authors have been added, what each author’s updated score is, and if anything went wrong with the data gathering. It’s a nice, passive way to keep track of any issues that might potentially arise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This web app has been made possible by &lt;a href="https://nuxt.com/"&gt;Nuxt&lt;/a&gt; handling the web front/backend, &lt;a href="https://tailwindui.com/"&gt;Tailwind&lt;/a&gt; to make it pretty and &lt;a href="https://vercel.com/"&gt;Vercel&lt;/a&gt; to host it, &lt;a href="https://go.dev/"&gt;Go&lt;/a&gt; to implement the script to scrape article data, &lt;a href="https://supabase.com/"&gt;Supabase&lt;/a&gt; to store everything in a managed Postgres instance and provide an authentication system, &lt;a href="https://www.linode.com/"&gt;Linode&lt;/a&gt; for a stable and cheap VPS, and last but certainly not least, &lt;a href="https://huggingface.co/"&gt;Hugging Face&lt;/a&gt; with the terrific AI solutions.&lt;/p&gt;

&lt;p&gt;One unexpected thing I realized once the site went live was I now read more news articles than I did in the past. The short summaries help me identify what I really care about and having links to multiple sources in the same place makes it faster to jump to the articles I find interesting.&lt;/p&gt;

&lt;p&gt;I hope this has been somewhat informational or at least interesting to read. If you have any questions, feedback, or think I may be able to help in any way, don’t hesitate to get in touch!&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
    <item>
      <title>Easy blogging</title>
      <dc:creator>Tiny</dc:creator>
      <pubDate>Mon, 17 Apr 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/tinygamedev/easy-blogging-1042</link>
      <guid>https://dev.to/tinygamedev/easy-blogging-1042</guid>
      <description>&lt;h2&gt;
  
  
  I've got a great blog, where do I host it?
&lt;/h2&gt;

&lt;p&gt;Well, you don't need to host it, you can use &lt;a href="https://dev.to/"&gt;dev.to&lt;/a&gt;, obviously! But let's say you like to tinker and do want to maintain your own blog.&lt;/p&gt;

&lt;p&gt;If you’re a dev, or a bit techy and don’t mind getting your hands dirty, there’s a nice and quick way to set up a blog deployment system with &lt;a href="https://github.com/"&gt;GitHub&lt;/a&gt; and &lt;a href="https://www.cloudflare.com/"&gt;Cloudfare&lt;/a&gt;. It’s simple, free, and relatively quick. There’s better ways to get a blog out there if you want simplicity, but I’ve found doing it this way to be more fulfilling.&lt;/p&gt;

&lt;p&gt;We’re only talking about deploying the blog here, so I’m assuming you have a way to build the content already. &lt;a href="https://tinygame.dev"&gt;My own blog&lt;/a&gt;, for example, uses the terrific &lt;a href="https://astro.build/"&gt;Astro&lt;/a&gt; framework.&lt;/p&gt;

&lt;p&gt;First step, once you have your blog code set up and you can create your posts, is to chuck everything onto GitHub. With the repo ready to go, create a project in Cloudflare Pages and connect to Git. Get the GitHub and Cloudflare accounts connected, make sure the Cloudflare app on GitHub has permission to see your blog repo, and select it in your Pages project. Lastly, set up the build settings for your project if relevant (e.g. &lt;code&gt;npm run build&lt;/code&gt; for the build command and select the destination folder).&lt;/p&gt;

&lt;p&gt;That’s basically it. All you need to do now is create your blog posts and push to GitHub. The Cloudflare app will sync, build, and deploy the new content for each commit.&lt;/p&gt;

</description>
      <category>blog</category>
    </item>
    <item>
      <title>Gaming news with a twist</title>
      <dc:creator>Tiny</dc:creator>
      <pubDate>Mon, 10 Apr 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/tinygamedev/gaming-news-with-a-twist-534j</link>
      <guid>https://dev.to/tinygamedev/gaming-news-with-a-twist-534j</guid>
      <description>&lt;h2&gt;
  
  
  What's happening?
&lt;/h2&gt;

&lt;p&gt;Friends, I apologize, this is a bit of a distraction from making games. It’s sort of related, but not really, and feels a lot like an excuse not to work on &lt;a href="https://tinygame.dev/tags/magivoid/"&gt;Magivoid&lt;/a&gt;. I’ve still been building things though, and as long as you learn new things it’s a win in my book.&lt;/p&gt;

&lt;p&gt;Today I’d like to introduce &lt;a href="https://gamebiz.news"&gt;gamebiz.news&lt;/a&gt;, a gaming news aggregator where you can up/down vote the authors. These people rate our games, why shouldn’t we rate them?&lt;/p&gt;

&lt;p&gt;On a serious note, I added the voting mechanic mostly for fun. The main reason for the site is to quench my thirst for knowing what’s happening in the game industry. There’s a lot of good writers out there and it’s always been a bit annoying to browse multiple websites. Now they’re all in one place…ish. The site covers only a handful of websites for now. I may add more in the future if needed, but there’s a good amount of articles being posted each day already and I feel this covers the industry well enough.&lt;/p&gt;

&lt;p&gt;Why not just use an RSS reader, I hear you ask? Where’s the fun in that? Building this site allowed me to play with all kinds of new tech. It’s got a &lt;a href="https://nuxt.com/"&gt;nuxt3&lt;/a&gt; front/backend, a golang article aggregator, and it even runs the text through an AI model to create summaries (which turned out surprisingly good).  I’ll try to find time to write a post about the technical details next.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Perforce and Notepad++</title>
      <dc:creator>Tiny</dc:creator>
      <pubDate>Fri, 10 Mar 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/tinygamedev/perforce-and-notepad-530i</link>
      <guid>https://dev.to/tinygamedev/perforce-and-notepad-530i</guid>
      <description>&lt;h2&gt;
  
  
  Increment Notepad
&lt;/h2&gt;

&lt;p&gt;Some Perforce commands will require you to input some info. Operations like changing the permissions with &lt;code&gt;p4 protect&lt;/code&gt; or creating a changelist with &lt;code&gt;p4 change&lt;/code&gt; for example. An alternative is to the the p4v GUI of course, but occasionally you might want to run them from the command line. In these cases, perforce will launch a text editor for you to make the change in. By default this will be Notepad. If you're on Linux or Mac it will be vi (lucky you), but if you're making games chances are you'll be on Windows.&lt;/p&gt;

&lt;p&gt;We can change the editor perforce uses with the P4EDITOR environment variable. Notepad++ is a good option and what I use. For this to behave well though, you'll need to add the &lt;code&gt;-multiInst&lt;/code&gt; and &lt;code&gt;-nosession&lt;/code&gt; arguments. The former to allow Notepad++ to launch a separate, temporary session exclusively for perforce, and the latter to avoid including all your currently open tabs in this temporary session.&lt;/p&gt;

&lt;p&gt;The full command line looks like this: &lt;code&gt;p4 set P4EDITOR="C:\Program Files\Notepad++/Notepad++.exe -multiInst -nosession"&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>perforce</category>
    </item>
    <item>
      <title>Perforce server</title>
      <dc:creator>Tiny</dc:creator>
      <pubDate>Wed, 08 Mar 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/tinygamedev/perforce-server-1amf</link>
      <guid>https://dev.to/tinygamedev/perforce-server-1amf</guid>
      <description>&lt;h2&gt;
  
  
  You need source control
&lt;/h2&gt;

&lt;p&gt;If you're not using source control, start using source control. All game companies do this, small or large. You should be doing it as well.&lt;/p&gt;

&lt;p&gt;The most basic features will immediately be useful. You'll get revision history and will be able to diff and see what's changed. You'll have a much easier time collaborating with others if you want or need to do that. If you set up a server off-site, it also acts as a solid backup in case something happens to your system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Perforce
&lt;/h2&gt;

&lt;p&gt;Game companies working on anything other than trivial projects will most likely use &lt;a href="https://www.perforce.com"&gt;Perforce&lt;/a&gt;. It can grow to be expensive but the license is free for 5 seats so you might not need to worry about it. Git is popular with programmers but when you deal with large binary assets the workflow tends to cause issues. Binary assets can't be diffed (most of the time) and you can't merge them. Even if you don't collaborate with others heavily and don't rely on Git's distributed workflow, large binary files might cause issues and you'll need to deal with &lt;a href="https://git-lfs.com"&gt;LFS&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For &lt;a href="https://tinygame.dev/tags/magivoid/"&gt;Magivoid&lt;/a&gt; I set up a Perforce server on a 1 GB Nanode running Ubuntu on &lt;a href="https://www.linode.com/pricing/#compute-shared"&gt;Linode&lt;/a&gt;. This is the tiniest shared VM they have and the performance is enough for my use. The 25 GB storage handles 10 GB of Magivoid code and data just fine for now. If you don't want to spend $5 per month for your server you can use any old PC you might have at home. The recommended spec for a Perforce server are &lt;a href="https://portal.perforce.com/s/article/2957"&gt;fairly modest&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>perforce</category>
    </item>
    <item>
      <title>Magivoid Devlog #2 - Teams, AI, combat, and a thingamajig</title>
      <dc:creator>Tiny</dc:creator>
      <pubDate>Wed, 01 Feb 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/tinygamedev/magivoid-devlog-2-teams-ai-combat-and-a-thingamajig-26np</link>
      <guid>https://dev.to/tinygamedev/magivoid-devlog-2-teams-ai-combat-and-a-thingamajig-26np</guid>
      <description>&lt;h2&gt;
  
  
  Teams
&lt;/h2&gt;

&lt;p&gt;With a landscape and a character shooting fireballs in place, it’s time to add things that a game might have. Continuing our multiplayer MOBA-like concept, a solid addition at this point is the notion of teams. Luckily this was easy to do with the gameplay tag system in Unreal Engine. These tags are essentially just labels but implemented in a fairly performant way with indexed strings. I think they’re also cheap to replicate across the network, but I haven’t actually looked close at this.&lt;/p&gt;

&lt;p&gt;I set up two bases, one red and one blue, with a lane between them and a thingamajig in the center. The idea for the goal of the game is to destroy the other team’s base. There will be minions spawning at specific intervals from each base, walking down the lane, and fighting any enemies they encounter. Once the minions reach the enemy base, they engage in destroying it. Simple and recognizable formula. I’m not sure what the thingamajig in the center will end up being, but I like the idea of having some object where the minions will presumably first encounter each other (unless the players intervene earlier).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fh3JANEk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uh8wo6fl6zqjqg65lquc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fh3JANEk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uh8wo6fl6zqjqg65lquc.png" alt="Our so called map" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  AI
&lt;/h2&gt;

&lt;p&gt;The gameplay tags make it really easy to propagate properties through various code systems. I’ve already used the team tags in a few different places. First, there’s two spawn points, one at each base. The player character gets a specific team tag applied based on what spawn point they spawn at. Second, the base itself has a team tag applied to it. This will stop players damaging their own base, accidentally or otherwise. Third, the minions that spawn have a team tag applied. This will be used by the AI to identify who is and who isn’t an enemy. Fourth, and lastly for now, I use team tags on my waypoint actors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eDjZ-z4Y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5ao6jsekzzx3cjtpex7l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eDjZ-z4Y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5ao6jsekzzx3cjtpex7l.png" alt="Waypoints in the editor" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The waypoints are just blueprints in UE that store an index and belong to a team. The index is used by the AI to know which waypoint to move towards. The minions, for now, have very simple behaviors: move down the list of waypoints and stop and shoot when an enemy is within range. If the minion gets to the enemy base, stop moving and start shooting until either the base or minion is destroyed.&lt;/p&gt;

&lt;p&gt;You’ll notice in the screenshot above the waypoints for the blue team are just slightly above the waypoints for the red team. The behavior tree for the minion AI uses EQS queries to get random points around each waypoint for the minion to move towards so it’s always a slightly different path, though probably not noticeable. This should be enough for multiple minions to not pick the same location and bump into each other. In fact, minions from opposing teams wouldn’t bump into each other anyway as they would stop and shoot before they got close enough to touch.&lt;/p&gt;

&lt;p&gt;Speaking of minions bumping into each other, right now there’s only one minion spawning at a time for each team. In traditional MOBAs there tends to be a set of minions walking together. Something to consider for later, but this will have to come once we can balance the gameplay a bit anyway, otherwise we’re just guessing what works at this point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combat
&lt;/h2&gt;

&lt;p&gt;The minions have a continuous enemy detection scan going that’s basically a simple visibility and distance check for player and AI actors. When an actor tagged as the opposing team is within range, the minion stops walking and starts shooting at that actor. The attack loop fires off three shots with a 0.5s delay between each, then a 1.5s delay, and repeat. The timing will likely need tweaking at some point once we start looking at damage values.&lt;/p&gt;

&lt;p&gt;You might also have noticed in the screenshot above how the red and blue waypoints diverge around our thingamajig centerpiece. The red AI takes one path and the blue AI takes the other. When the minions from the two teams are circling around this area, they are actually within each other’s attack range. The cone however blocks their line of sight, which causes them to continue walking towards their next waypoint as if nothing happened. &lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;p&gt;Technically that’s correct, they don’t see each other. This is purely an accident due to the minimal effort I put into creating the cone that serves as the centerpiece. If we replace it with a smaller object (or remove it), the minions would see each other and engage in combat.&lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;p&gt;I quite like the outcome of the minions not seeing each other. It means by default they bypass the encounter in the center and reach all the way to the enemy base. Unless, of course, one or both of the players interject. One option is to sprint (which we support through ALS) to reach the enemy minion and engage them in combat, causing them to stop and allow your own minion to advance farther. I hope to find additional ways to indirectly influence the AI behavior, but again this would be something for later.&lt;/p&gt;

&lt;p&gt;A second unintended “feature” noticeable in the video above is the collision between minion projectiles. They don’t pass through each other so you get cases where they explode in mid-air.  The minion aim is randomized a touch at the moment (see &lt;a href="https://tinygame.dev/posts/Simple-bullet-spread-for-AI/"&gt;this blog post&lt;/a&gt; for implementation details), so they don’t always collide. However, the projectile bounding sphere is fairly large and collisions do happen somewhat regularly. I love this result. As the great Mr Ross would say, we don’t make mistakes, just happy little accidents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.unrealengine.com/5.1/en-US/gameplay-tags-in-unreal-engine/"&gt;Gameplay tags&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.unrealengine.com/5.1/en-US/environment-query-system-in-unreal-engine/"&gt;Environment Query System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.unrealengine.com/marketplace/en-US/product/paragon-minions"&gt;Paragon: Minions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://courses.tomlooman.com/p/unrealengine-cpp?coupon_code=COMMUNITY15"&gt;Professional Game Development in C++ and Unreal Engine (great course on UE)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gamedev</category>
      <category>devlog</category>
    </item>
    <item>
      <title>Magivoid Devlog #1 - Terrain, locomotion, fireball!</title>
      <dc:creator>Tiny</dc:creator>
      <pubDate>Mon, 30 Jan 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/tinygamedev/magivoid-devlog-1-terrain-locomotion-fireball-5e93</link>
      <guid>https://dev.to/tinygamedev/magivoid-devlog-1-terrain-locomotion-fireball-5e93</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;In the last post I mentioned not knowing what game to make. This isn’t entirely true. I don’t have a solid design or specific goal to achieve, but I do have some thoughts on which direction to move towards to get things started.&lt;/p&gt;

&lt;p&gt;To begin with, this will be a third person game made in Unreal Engine 5. The idea is to make it multiplayer in order to keep the scope narrow. One map, one game mode, a small amount of assets, endless fun. Sounds reasonable, right? I realize “multiplayer” and “narrow scope” in the same sentence might be hard to digest, but Unreal offers a pretty solid foundation. With this setup we also have the ability to easily extend the game with additional maps, modes, and items, if, through some miracle, the game is completed.&lt;/p&gt;

&lt;p&gt;Multiplayer-wise I’m thinking it’ll be session-based (obviously) and probably low player count. Perhaps some sort of a MOBA-like in a 4v4 fashion. I believe Steam can be used to launch a game like this and get a bit of help when it comes to server discovery and whatnot. Maybe I’ll even do an early access release and distribute builds that way. But, I’m putting the cart before the horse now. Let’s start by looking at some basic systems I put in place so far.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terrain
&lt;/h2&gt;

&lt;p&gt;For the environment I was considering some sort of island to justify a small environment. Having everything surrounded by water and the familiar protagonist player character that never learned to swim gives the entire thing a believable vibe. But I don’t really want to deal with water to be entirely honest. Maybe we’ll do a small pond or a creek at some point for aesthetics if everything else goes smoothly, but for now I decided to keep it simple and ignore what the edge of the world looks like.&lt;/p&gt;

&lt;p&gt;With the type of game being a MOBA-like I’m imagining we’ll have some sort of lane on which AI will move. This led me to think about putting the entire gameplay within a canyon surrounded by steep slopes on each side that block player movement. The cliffs would neatly handle two of the four sides of the map in the “edge of the world” conundrum. We’ll figure something out for the other two sides later.&lt;/p&gt;

&lt;p&gt;I considered a couple of different tools for generating the terrain and decided to go with Gaea. It works great and has a nice and easy workflow to export terrain heightmaps and texture masks to Unreal. The node-based generator makes it super simple to quickly generate really cool looking terrains. Here’s the graph I ended up with and the result it generated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6hfD4qnT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wtwibnfuzatwps6v6vmj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6hfD4qnT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wtwibnfuzatwps6v6vmj.png" alt="Gaea graph and result" width="800" height="674"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gaea has presets for exporting maps of the correct size required by Unreal, and with a bit of math to set up proper scale after import into the editor you get a nice looking landscape for your game.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--A9Df_M7a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ckwpgsujwr1b1rrdczig.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9Df_M7a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ckwpgsujwr1b1rrdczig.png" alt="Terrain imported into UE5" width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With Gaea you can use a color palette selector to visualize terrain materials but this doesn’t get transferred to Unreal. You can optionally export various masks and use them to apply different materials to your terrain once you get it into the editor. This is cool for things like erosion but specifying where different terrain materials should be applied with a mask felt too restrictive. Instead I went with a slope-based blend material to get the above result. It has the added benefit of “just working” if we later modify the terrain in Unreal, which is possible since we’re dealing with a heightmap at this point.&lt;/p&gt;

&lt;p&gt;The heightmap resolution for the above result is 1k and scaled to cover a 1km^2 area. The 1m/texel resolution is a little bit low I’ve noticed, and the 1km^2 area is a bit too large for what I had in mind. I could re-export it all from Gaea at a different resolution and change it, or just make the terrain cover a smaller area, but I’m lazy and decided to keep it like this for now. We’ll see if I get back to this at some point. I figured it would be easy to cut away terrain if the area is too large.&lt;/p&gt;

&lt;p&gt;Unfortunately, the low resolution of the heightmap can cause visually jarring artifacts due to self-shadowing, but it’s an easy fix with the smooth tool inside UE.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rgWDD69V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yquiw4wc72xnbemvzllo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rgWDD69V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yquiw4wc72xnbemvzllo.png" alt="Terrain jaggies before and after" width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Locomotion
&lt;/h2&gt;

&lt;p&gt;With the game being in third person it’s safe to assume the player character will be somewhat important. At least it needs to not be annoying since it’s one of the things you’ll see the most. Not to mention the way the character interacts with the world will directly contribute to how the game feels to control. Now, I don’t have anything against the default setup that comes with UE. It’s a solid and responsive system. I only prefer something slightly less arcadey for this project.&lt;/p&gt;

&lt;p&gt;To solve the arcadey feel I added the Advanced Locomotion System plugin. There’s a bunch of cool features in there and even using only the basic locomotion animations gives an improved result for character control.&lt;/p&gt;


  


&lt;p&gt;Lastly, I added a fireball attack because this is a game after all. It doesn’t do much beyond spawning a sweet looking projectile that explodes on impact, but it’s a start. Epic, the beautiful humans they are, released quite a lot of assets for free from their Paragon game. I ended up using a nicely fitting animation from one of their characters for the fireball launch, which goes to show how easy Unreal Engine is to work with when it comes to sharing and reusing assets.&lt;/p&gt;


  


&lt;p&gt;Funnily enough this attack feature exposed an interesting issue in third person games. Namely, how do you make a projectile hit what the player thinks they’re aiming at? It’s a harder problem to solve well than you’d expect. Mostly because the player aims through the center of the screen but the projectile travels from a different location (e.g. the player’s hand in this case) towards the target. For that matter, how do you know what the target needs to be?&lt;/p&gt;

&lt;p&gt;I went with tracing a line from the camera out into the world (i.e towards what the player sees in the center of the screen) and based on what it hits try to guess what the player meant to aim at. If the trace hits nothing, pretend there’s something X meters away and launch the projectile in that direction. It works fairly well, but there are edge cases where it breaks. Perhaps that will be a problem to solve at some point but for now this will do.&lt;/p&gt;

&lt;p&gt;Until next time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://quadspinner.com"&gt;Gaea&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.epicgames.com/community/learning/tutorials/KJ7l/landscape-import-basics"&gt;Landscape import basics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dyanikoglu/ALS-Community"&gt;Advanced Locomotion System - Community Version&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.unrealengine.com/marketplace/en-US/product/dreamscape-nature-meadows"&gt;Dreamscape Nature: Meadows (landscape material)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.unrealengine.com/marketplace/en-US/product/aki-stylized-character"&gt;Aki - Stylized character&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.unrealengine.com/marketplace/en-US/product/improve-fights-stylized-vfx-pack"&gt;Improve fights - Stylized VFX pack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.unrealengine.com/marketplace/en-US/product/paragon-gideon"&gt;Paragon: Gideon&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gamedev</category>
      <category>devlog</category>
    </item>
    <item>
      <title>Magivoid Devlog #0 - I would like to make a game</title>
      <dc:creator>Tiny</dc:creator>
      <pubDate>Tue, 24 Jan 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/tinygamedev/magivoid-devlog-0-i-would-like-to-make-a-game-53af</link>
      <guid>https://dev.to/tinygamedev/magivoid-devlog-0-i-would-like-to-make-a-game-53af</guid>
      <description>&lt;h2&gt;
  
  
  Why?
&lt;/h2&gt;

&lt;p&gt;I started messing about with computers about three decades ago (yikes, 2023), and I quickly gravitated towards building games. They weren’t really games at that point, but in my head that was always the goal. It was never apps or utilities, although various tools and scripts always ended up being coded to help speed things up or automate things. There was just something alluring about games.&lt;/p&gt;

&lt;p&gt;I had played games for years before but that was on consoles like the NES and Master System with no option to explore programming. Once I got my hands on my first computer, I never looked back.&lt;/p&gt;

&lt;p&gt;I don’t remember clearly how long various phases took, but it felt like a long time passed where I just messed around with QBasic and later Visual Basic. I remember enjoying it but feeling frustrated that it didn’t let me build what I wanted quickly enough. I somehow ended up using tools like &lt;a href="https://www.youtube.com/watch?v=GYvoBc1qW8Y"&gt;Klick&lt;br&gt;
&amp;amp; Play&lt;/a&gt; and later its successor &lt;a href="https://www.youtube.com/watch?v=Y6F_rpBGRbg"&gt;The Games Factory&lt;/a&gt;. It was fun times and many 2D prototypes were built, but never full games.&lt;/p&gt;

&lt;p&gt;Once I hit the boundaries of what those early game maker tools could do, I went back to programming. I had a thing for JRPGs on the SNES, so naturally I attempted with little success to render tile based maps in a form in Visual Basic 6. That struggle lead to trying to learn C, which ended horribly and instead I went to Java and learned to build applets. I don’t remember the details, but I remember enjoying being able to render 2d graphics more easily. Eventually I went back to C and C++ and managed to learn those too.&lt;/p&gt;

&lt;p&gt;I studied computer science and my first job out of university was a gamedev job. My path since I was a kid has been a fairly straight flying arrow into the game industry and I’ve loved every moment of it. By now I’ve shipped a few significant AAA titles and have touched my fair share of code systems. Yet, I haven’t built a game from start to finish by myself. There’s an endless graveyard of prototypes, demos, designs, and ideas, but no project that I could call completed.&lt;/p&gt;

&lt;p&gt;I would like to change this now. I would like to make a game.&lt;/p&gt;

&lt;h2&gt;
  
  
  What?
&lt;/h2&gt;

&lt;p&gt;I don’t know what game I will make yet. It will likely be relatively small in scope so it can be finished. It will use Unreal Engine 5 and probably a lot of assets from the UE Marketplace. Not knowing exactly what the finished product should be is somewhat liberating. It means I can pivot, experiment and try different things until something works. We will see.&lt;/p&gt;

&lt;p&gt;At this point in my career I know what it takes to build a game. Even as I type this I’m unsure if I will actually complete it. But who knows, maybe this time it will happen.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>devlog</category>
    </item>
  </channel>
</rss>
