Jekyll2020-10-03T20:37:44+00:00https://nightowl.games/feed.xmlnightowlWelcome to nightowl games.JasonMenu Polish in IsoPutt2020-10-03T20:30:00+00:002020-10-03T20:30:00+00:00https://nightowl.games/blog/isoputt/menu-polish-in-isoputt<p><img src="/assets/img/isoputt_menupolish.gif" alt="isoputt_menupolish" title="Menu UI Gif" /><br />
<em>Color palette swapping makes this menu come alive.</em></p>
<p>I’ve been working on the menus for IsoPutt for almost 4 months now. It can be hard to stay motivated when working on “boring” stuff like this! It’s not as exciting as working on new gameplay features or coding elaborate systems…</p>
<p>But I’ve seen first hand how much impact this has had on the way people perceive the game. When you have an ugly menu, you kind of start “in debt” when the player makes it to the gameplay. The gameplay has to overcome that “menu debt” before it can be judged on it’s merits. When you have a nice menu, players seem to have a less critical, more open, mindset.</p>
<p>I’ve noticed play testers playing longer, and expressing more joy now that I’ve improved the menu systems.</p>
<p>The changing color palettes serves as a tangible reward for having completed a course. It feels like the menu system was the X factor that IsoPutt has been needing.</p>
<p>This has all come together towards a lesson of going deep into your game’s design, and refusing to get caught into the trap of breadth.</p>
<p>I’m closing in on having a releasable product…</p>JasonColor palette swapping makes this menu come alive.Ball Trails in IsoPutt2020-04-27T00:50:00+00:002020-04-27T00:50:00+00:00https://nightowl.games/blog/isoputt/ball-trails-in-isoputt<p>I’ve added a ball trail to <a href="/isoputt">IsoPutt</a>. It serves two purposes: one, it’s like how in Super Meat Boy you can see all of your failed attempts. In IsoPutt, the ball trail doesnt reset when you fail the level, so you see all the lines you’ve made.</p>
<p>Additionally, I’m trying to make the ground look a bit more like grass. I think I have some further iteration to do on this… perhaps using a foliage texture for the brush on the ground.</p>
<p>This post describes how I created this ball trail effect in <a href="/isoputt">IsoPutt</a> using the <a href="https://godotengine.org/">Godot</a> engine. At a high level, it involves using barycentric coordinates to transform a world position into a uv coordinate, then uses a shader on the green with a decal map, two color indicies, and a palette texture in order to draw the trails.</p>
<p><img src="/assets/img/isoputt_ball_trail.gif" alt="isoputt_ball_trail" title="Ball Trail Gif" /><br />
<em>The ball’s touch darkens the green texture underneath it.</em></p>
<h1 id="drawing-under-the-ball">Drawing Under the Ball</h1>
<p>In order to figure out where on the green’s UV map to draw, I needed to convert the ball’s collision position into a UV coordinate.</p>
<p>In the ball’s <code class="language-plaintext highlighter-rouge">_physics_process</code>, we have this block of code:</p>
<figure class="highlight"><pre><code class="language-gdscript" data-lang="gdscript"><span class="c1"># Draw ball trails on the green</span>
<span class="k">if</span> <span class="n">physics_material_name</span> <span class="o">==</span> <span class="s2">"green"</span><span class="p">:</span>
<span class="k">var</span> <span class="n">ball_trail</span> <span class="p">:</span><span class="o">=</span> <span class="n">collision</span><span class="o">.</span><span class="n">collider</span><span class="o">.</span><span class="n">get_node</span><span class="p">(</span><span class="s2">"../"</span><span class="p">)</span> <span class="k">as</span> <span class="n">BallTrail</span>
<span class="k">if</span> <span class="n">ball_trail</span> <span class="o">!=</span> <span class="kt">null</span><span class="p">:</span>
<span class="n">ball_trail</span><span class="o">.</span><span class="n">draw_on_point</span><span class="p">(</span><span class="n">collision</span><span class="o">.</span><span class="n">position</span><span class="p">)</span></code></pre></figure>
<p>The BallTrail class is a script extending from MeshInstance. If ball touches the green, it looks for the BallTrail node, and if it finds it, calls <code class="language-plaintext highlighter-rouge">draw_on_point</code>.</p>
<p><code class="language-plaintext highlighter-rouge">draw_on_point</code> uses barycentric coordinates to find which triangle the ball is on, then gets the UV coordinates and calls into <code class="language-plaintext highlighter-rouge">draw_on_uv</code> in order to draw a spot on the decal map. Note that this function, and <code class="language-plaintext highlighter-rouge">draw_on_uv</code>, only really work on the default Godot cube. To extend them to a more general solution, you’d have to find if the brush lies on <em>multiple triangles</em>. IsoPutt’s greens are all secretely just cubes, so it works well enough for this game.</p>
<p>This function uses the <a href="https://docs.godotengine.org/en/latest/classes/class_meshdatatool.html">MeshDataTool</a> class to access the vertex and triangle data. That class provides a nice interface over raw mesh data access.</p>
<figure class="highlight"><pre><code class="language-gdscript" data-lang="gdscript"><span class="c1"># Draws on the decal map at point p</span>
<span class="k">func</span> <span class="nf">draw_on_point</span><span class="p">(</span><span class="n">p</span><span class="p">:</span><span class="kt">Vector3</span><span class="p">):</span>
<span class="k">var</span> <span class="n">t</span><span class="p">:</span><span class="kt">Transform</span> <span class="o">=</span> <span class="n">global_transform</span>
<span class="c1"># Iterate over every triangle in the mesh</span>
<span class="k">for</span> <span class="n">triangle_index</span> <span class="ow">in</span> <span class="n">mesh_data_tool</span><span class="o">.</span><span class="n">get_face_count</span><span class="p">():</span>
<span class="k">var</span> <span class="n">a_index</span> <span class="p">:</span><span class="o">=</span> <span class="n">mesh_data_tool</span><span class="o">.</span><span class="n">get_face_vertex</span><span class="p">(</span><span class="n">triangle_index</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
<span class="k">var</span> <span class="n">b_index</span> <span class="o">=</span> <span class="n">mesh_data_tool</span><span class="o">.</span><span class="n">get_face_vertex</span><span class="p">(</span><span class="n">triangle_index</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="k">var</span> <span class="n">c_index</span> <span class="p">:</span><span class="o">=</span> <span class="n">mesh_data_tool</span><span class="o">.</span><span class="n">get_face_vertex</span><span class="p">(</span><span class="n">triangle_index</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
<span class="k">var</span> <span class="n">a</span> <span class="p">:</span><span class="o">=</span> <span class="n">mesh_data_tool</span><span class="o">.</span><span class="n">get_vertex</span><span class="p">(</span><span class="n">a_index</span><span class="p">)</span>
<span class="k">var</span> <span class="n">b</span> <span class="p">:</span><span class="o">=</span> <span class="n">mesh_data_tool</span><span class="o">.</span><span class="n">get_vertex</span><span class="p">(</span><span class="n">b_index</span><span class="p">)</span>
<span class="k">var</span> <span class="n">c</span> <span class="p">:</span><span class="o">=</span> <span class="n">mesh_data_tool</span><span class="o">.</span><span class="n">get_vertex</span><span class="p">(</span><span class="n">c_index</span><span class="p">)</span>
<span class="n">a</span> <span class="o">=</span> <span class="n">t</span><span class="o">.</span><span class="n">xform</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
<span class="n">b</span> <span class="o">=</span> <span class="n">t</span><span class="o">.</span><span class="n">xform</span><span class="p">(</span><span class="n">b</span><span class="p">)</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">t</span><span class="o">.</span><span class="n">xform</span><span class="p">(</span><span class="n">c</span><span class="p">)</span>
<span class="c1"># A dumb, unoptimized triangle/point intersection test</span>
<span class="k">var</span> <span class="n">bary</span> <span class="p">:</span><span class="o">=</span> <span class="n">barycentric</span><span class="p">(</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">,</span><span class="n">c</span><span class="p">,</span><span class="n">p</span><span class="p">)</span>
<span class="k">var</span> <span class="n">bary_sum</span> <span class="p">:</span><span class="o">=</span> <span class="n">bary</span><span class="o">.</span><span class="n">x</span> <span class="o">+</span> <span class="n">bary</span><span class="o">.</span><span class="n">y</span> <span class="o">+</span> <span class="n">bary</span><span class="o">.</span><span class="n">z</span>
<span class="k">if</span> <span class="n">bary</span><span class="o">.</span><span class="n">x</span> <span class="o">>=</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">bary</span><span class="o">.</span><span class="n">x</span> <span class="o"><=</span> <span class="mf">1.0</span> \
<span class="ow">and</span> <span class="n">bary</span><span class="o">.</span><span class="n">y</span> <span class="o">>=</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">bary</span><span class="o">.</span><span class="n">y</span> <span class="o"><=</span> <span class="mf">1.0</span> \
<span class="ow">and</span> <span class="n">bary</span><span class="o">.</span><span class="n">z</span> <span class="o">>=</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">bary</span><span class="o">.</span><span class="n">z</span> <span class="o"><=</span> <span class="mf">1.0</span><span class="p">:</span>
<span class="k">if</span> <span class="nb">abs</span><span class="p">(</span><span class="mf">1.0</span> <span class="o">-</span> <span class="n">bary_sum</span><span class="p">)</span> <span class="o"><</span> <span class="mf">0.001</span><span class="p">:</span>
<span class="k">var</span> <span class="n">uv_a</span> <span class="p">:</span><span class="o">=</span> <span class="n">mesh_data_tool</span><span class="o">.</span><span class="n">get_vertex_uv</span><span class="p">(</span><span class="n">a_index</span><span class="p">)</span>
<span class="k">var</span> <span class="n">uv_b</span> <span class="p">:</span><span class="o">=</span> <span class="n">mesh_data_tool</span><span class="o">.</span><span class="n">get_vertex_uv</span><span class="p">(</span><span class="n">b_index</span><span class="p">)</span>
<span class="k">var</span> <span class="n">uv_c</span> <span class="p">:</span><span class="o">=</span> <span class="n">mesh_data_tool</span><span class="o">.</span><span class="n">get_vertex_uv</span><span class="p">(</span><span class="n">c_index</span><span class="p">)</span>
<span class="k">var</span> <span class="n">uv</span> <span class="p">:</span><span class="o">=</span> <span class="n">barycentric_to_uv</span><span class="p">(</span><span class="n">uv_a</span><span class="p">,</span> <span class="n">uv_b</span><span class="p">,</span> <span class="n">uv_c</span><span class="p">,</span> <span class="n">bary</span><span class="p">)</span>
<span class="n">draw_on_uv</span><span class="p">(</span><span class="n">uv</span><span class="p">)</span>
<span class="k">return</span></code></pre></figure>
<p>Note that the <a href="https://docs.godotengine.org/en/latest/classes/class_meshdatatool.html">MeshDataTool</a> only works on <code class="language-plaintext highlighter-rouge">ArrayMesh</code>, so if you have a different mesh (eg: <code class="language-plaintext highlighter-rouge">CubeMesh</code>), then you must convert it to an array mesh first by using some code like this:</p>
<figure class="highlight"><pre><code class="language-gdscript" data-lang="gdscript"><span class="k">func</span> <span class="nf">create_mesh_data_tool</span><span class="p">(</span><span class="n">mesh</span><span class="p">:</span><span class="n">Mesh</span><span class="p">)</span> <span class="o">-></span> <span class="n">MeshDataTool</span><span class="p">:</span>
<span class="k">var</span> <span class="n">array_mesh</span> <span class="o">=</span> <span class="n">mesh</span>
<span class="k">if</span> <span class="o">!</span><span class="n">array_mesh</span> <span class="k">is</span> <span class="n">ArrayMesh</span><span class="p">:</span>
<span class="k">var</span> <span class="n">surface_arrays</span> <span class="o">=</span> <span class="n">mesh</span><span class="o">.</span><span class="n">surface_get_arrays</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
<span class="n">array_mesh</span> <span class="o">=</span> <span class="n">ArrayMesh</span><span class="o">.</span><span class="n">new</span><span class="p">()</span>
<span class="n">array_mesh</span><span class="o">.</span><span class="n">add_surface_from_arrays</span><span class="p">(</span><span class="n">Mesh</span><span class="o">.</span><span class="n">PRIMITIVE_TRIANGLES</span><span class="p">,</span> <span class="n">surface_arrays</span><span class="p">)</span>
<span class="k">var</span> <span class="n">mesh_data_tool</span> <span class="o">=</span> <span class="n">MeshDataTool</span><span class="o">.</span><span class="n">new</span><span class="p">()</span>
<span class="n">mesh_data_tool</span><span class="o">.</span><span class="n">create_from_surface</span><span class="p">(</span><span class="n">array_mesh</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
<span class="k">return</span> <span class="n">mesh_data_tool</span></code></pre></figure>
<h1 id="barycentric-coordinates">Barycentric Coordinates</h1>
<p>Barycentric coordinates are a way to represent a point on a triangle. I learnt what they were by reading this great writeup by <a href="https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-rendering-a-triangle/barycentric-coordinates">scratch-a-pixel</a>. In essence, they are a bridge between the world space coordinate system and the uv space coordinate system.</p>
<figure class="highlight"><pre><code class="language-gdscript" data-lang="gdscript"><span class="c1"># Returns the barycentric coordinate for the point p on triangle (a,b,c)</span>
<span class="k">func</span> <span class="nf">barycentric</span><span class="p">(</span><span class="n">a</span><span class="p">:</span><span class="kt">Vector3</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span><span class="kt">Vector3</span><span class="p">,</span> <span class="n">c</span><span class="p">:</span><span class="kt">Vector3</span><span class="p">,</span> <span class="n">p</span><span class="p">:</span><span class="kt">Vector3</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Vector3</span><span class="p">:</span>
<span class="k">var</span> <span class="n">a0</span> <span class="p">:</span><span class="o">=</span> <span class="n">triangle_area</span><span class="p">(</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">,</span><span class="n">c</span><span class="p">)</span>
<span class="k">var</span> <span class="n">u</span> <span class="p">:</span><span class="o">=</span> <span class="n">triangle_area</span><span class="p">(</span><span class="n">p</span><span class="p">,</span> <span class="n">c</span><span class="p">,</span> <span class="n">a</span><span class="p">)</span> <span class="o">/</span> <span class="n">a0</span>
<span class="k">var</span> <span class="n">v</span> <span class="p">:</span><span class="o">=</span> <span class="n">triangle_area</span><span class="p">(</span><span class="n">p</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">a</span><span class="p">)</span> <span class="o">/</span> <span class="n">a0</span>
<span class="k">var</span> <span class="n">w</span> <span class="p">:</span><span class="o">=</span> <span class="n">triangle_area</span><span class="p">(</span><span class="n">p</span><span class="p">,</span> <span class="n">c</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span> <span class="o">/</span> <span class="n">a0</span>
<span class="k">return</span> <span class="kt">Vector3</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">u</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>
<span class="c1"># Returns the area of a triangle</span>
<span class="k">func</span> <span class="nf">triangle_area</span><span class="p">(</span><span class="n">a</span><span class="p">:</span><span class="kt">Vector3</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span><span class="kt">Vector3</span><span class="p">,</span> <span class="n">c</span><span class="p">:</span><span class="kt">Vector3</span><span class="p">)</span> <span class="o">-></span> <span class="kt">float</span><span class="p">:</span>
<span class="k">return</span> <span class="p">(</span><span class="n">b</span><span class="o">-</span><span class="n">a</span><span class="p">)</span><span class="o">.</span><span class="n">cross</span><span class="p">(</span><span class="n">c</span><span class="o">-</span><span class="n">a</span><span class="p">)</span><span class="o">.</span><span class="n">length</span><span class="p">()</span> <span class="o">*</span> <span class="mf">0.5</span></code></pre></figure>
<h1 id="uv-coordinates">UV Coordinates</h1>
<p>Once you can calculate the barycentric coordinate for a point inside a triangle, you can easily find the UV coordinate of any point inside the triangle. To map the barycentric coordinate onto UV space, you must provide the UV points for the 3 verticies of the triangle.</p>
<figure class="highlight"><pre><code class="language-gdscript" data-lang="gdscript"><span class="c1"># Returns the UV coordinate of the barycentric point p on the triangle (a, b, c)</span>
<span class="k">func</span> <span class="nf">barycentric_to_uv</span><span class="p">(</span><span class="n">a</span><span class="p">:</span><span class="kt">Vector2</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span><span class="kt">Vector2</span><span class="p">,</span> <span class="n">c</span><span class="p">:</span><span class="kt">Vector2</span><span class="p">,</span> <span class="n">p</span><span class="p">:</span><span class="kt">Vector3</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Vector2</span><span class="p">:</span>
<span class="k">return</span> <span class="n">a</span> <span class="o">*</span> <span class="n">p</span><span class="o">.</span><span class="n">x</span> <span class="o">+</span> <span class="n">b</span> <span class="o">*</span> <span class="n">p</span><span class="o">.</span><span class="n">y</span> <span class="o">+</span> <span class="n">c</span> <span class="o">*</span> <span class="n">p</span><span class="o">.</span><span class="n">z</span></code></pre></figure>
<h1 id="drawing-the-decal">Drawing the Decal</h1>
<p>Now we take the UV Coordinate and draw onto a black and white texture called a decal map. White means trail, black means no-trail. The decal map has to be the right size for the size of the mesh we are drawing on. For IsoPutt’s green cube meshes, I use the meshes globalscale * 26.5, such that a cube mesh with a scale of 10x10 has a 256x256 decal map.</p>
<p>Note that the brush size is 4x6. That’s what’s needed to draw squares on the default Godot CubeMesh’s UV Map. Pretty weird. 😐</p>
<figure class="highlight"><pre><code class="language-gdscript" data-lang="gdscript"><span class="k">func</span> <span class="nf">create_decal_map</span><span class="p">(</span><span class="n">uv_map_size</span><span class="p">:</span><span class="kt">Vector2</span><span class="p">)</span> <span class="o">-></span> <span class="n">Image</span><span class="p">:</span>
<span class="k">var</span> <span class="n">image</span> <span class="o">=</span> <span class="n">Image</span><span class="o">.</span><span class="n">new</span><span class="p">()</span>
<span class="n">image</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">uv_map_size</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="n">uv_map_size</span><span class="o">.</span><span class="n">y</span><span class="p">,</span> <span class="bp">false</span><span class="p">,</span> <span class="n">Image</span><span class="o">.</span><span class="n">FORMAT_R8</span><span class="p">)</span>
<span class="k">return</span> <span class="n">image</span>
<span class="k">func</span> <span class="nf">draw_on_uv</span><span class="p">(</span><span class="n">uv</span><span class="p">:</span><span class="kt">Vector2</span><span class="p">):</span>
<span class="k">var</span> <span class="n">width</span> <span class="p">:</span><span class="o">=</span> <span class="n">image</span><span class="o">.</span><span class="n">get_width</span><span class="p">()</span>
<span class="k">var</span> <span class="n">height</span> <span class="p">:</span><span class="o">=</span> <span class="n">image</span><span class="o">.</span><span class="n">get_height</span><span class="p">()</span>
<span class="c1"># Pixel coordinates from uv coordinate</span>
<span class="k">var</span> <span class="n">pixel_x</span> <span class="p">:</span><span class="o">=</span> <span class="kt">int</span><span class="p">(</span><span class="nb">round</span><span class="p">(</span><span class="n">uv</span><span class="o">.</span><span class="n">x</span> <span class="o">*</span> <span class="n">width</span><span class="p">))</span>
<span class="k">var</span> <span class="n">pixel_y</span> <span class="p">:</span><span class="o">=</span> <span class="kt">int</span><span class="p">(</span><span class="nb">round</span><span class="p">(</span><span class="n">uv</span><span class="o">.</span><span class="n">y</span> <span class="o">*</span> <span class="n">height</span><span class="p">))</span>
<span class="c1"># Default Cube's UV map has a width to height ratio of 2:3</span>
<span class="k">var</span> <span class="n">brush_size_x</span> <span class="p">:</span><span class="o">=</span> <span class="mi">4</span>
<span class="k">var</span> <span class="n">brush_size_y</span> <span class="p">:</span><span class="o">=</span> <span class="mi">6</span>
<span class="k">var</span> <span class="n">brush_min_x</span> <span class="p">:</span><span class="o">=</span> <span class="nb">round</span><span class="p">(</span><span class="nb">clamp</span><span class="p">(</span><span class="n">pixel_x</span> <span class="o">-</span> <span class="n">brush_size_x</span> <span class="o">*</span> <span class="mf">0.5</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">width</span><span class="p">))</span>
<span class="k">var</span> <span class="n">brush_max_x</span> <span class="p">:</span><span class="o">=</span> <span class="nb">round</span><span class="p">(</span><span class="nb">clamp</span><span class="p">(</span><span class="n">pixel_x</span> <span class="o">+</span> <span class="n">brush_size_x</span> <span class="o">*</span> <span class="mf">0.5</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">width</span><span class="p">))</span>
<span class="k">var</span> <span class="n">brush_min_y</span> <span class="p">:</span><span class="o">=</span> <span class="nb">round</span><span class="p">(</span><span class="nb">clamp</span><span class="p">(</span><span class="n">pixel_y</span> <span class="o">-</span> <span class="n">brush_size_y</span> <span class="o">*</span> <span class="mf">0.5</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">height</span><span class="p">))</span>
<span class="k">var</span> <span class="n">brush_max_y</span> <span class="p">:</span><span class="o">=</span> <span class="nb">round</span><span class="p">(</span><span class="nb">clamp</span><span class="p">(</span><span class="n">pixel_y</span> <span class="o">+</span> <span class="n">brush_size_y</span> <span class="o">*</span> <span class="mf">0.5</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">height</span><span class="p">))</span>
<span class="c1"># Draw on the Image</span>
<span class="n">image</span><span class="o">.</span><span class="n">lock</span><span class="p">()</span>
<span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="kt">int</span><span class="p">(</span><span class="n">brush_max_x</span> <span class="o">-</span> <span class="n">brush_min_x</span><span class="p">)):</span>
<span class="k">for</span> <span class="n">y</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="kt">int</span><span class="p">(</span><span class="n">brush_max_y</span> <span class="o">-</span> <span class="n">brush_min_y</span><span class="p">)):</span>
<span class="n">image</span><span class="o">.</span><span class="n">set_pixel</span><span class="p">(</span><span class="n">brush_min_x</span> <span class="o">+</span> <span class="n">x</span><span class="p">,</span> <span class="n">brush_min_y</span> <span class="o">+</span> <span class="n">y</span><span class="p">,</span> <span class="kt">Color</span><span class="o">.</span><span class="n">white</span><span class="p">)</span>
<span class="n">image</span><span class="o">.</span><span class="n">unlock</span><span class="p">()</span>
<span class="c1"># Write the image to a texture</span>
<span class="k">var</span> <span class="n">decal_texture</span> <span class="p">:</span><span class="o">=</span> <span class="n">ImageTexture</span><span class="o">.</span><span class="n">new</span><span class="p">()</span>
<span class="n">decal_texture</span><span class="o">.</span><span class="n">create_from_image</span><span class="p">(</span><span class="n">image</span><span class="p">)</span>
<span class="c1"># Set the texture on the material</span>
<span class="n">material</span><span class="o">.</span><span class="n">set_shader_param</span><span class="p">(</span><span class="s2">"decal_map"</span><span class="p">,</span> <span class="n">decal_texture</span><span class="p">)</span></code></pre></figure>
<h1 id="the-greens-shader">The Green’s Shader</h1>
<p>Now that we have the decal map, we need to sample it inside the green’s shader and select between the regular green color and the trail color. This shader uses a single color palette texture and two different color indicies to reference the green color and the trail color. It does this to integrate into IsoPutt’s color pallete system: <a href="https://github.com/jknightdoeswork/swatchd">swatchd</a>. This system allows you to reference centralized colors by name. It’s a great way to manage colors in a game, because then all the colors are editable from one centralized place.</p>
<figure class="highlight"><pre><code class="language-glsl" data-lang="glsl"><span class="n">shader_type</span> <span class="n">spatial</span><span class="p">;</span>
<span class="k">uniform</span> <span class="kt">sampler2D</span> <span class="n">decal_map</span><span class="p">;</span>
<span class="k">uniform</span> <span class="kt">sampler2D</span> <span class="n">color_palette</span><span class="o">:</span><span class="n">hint_albedo</span><span class="p">;</span>
<span class="k">uniform</span> <span class="kt">int</span> <span class="n">color_index</span><span class="p">;</span>
<span class="k">uniform</span> <span class="kt">int</span> <span class="n">color_index2</span><span class="p">;</span>
<span class="kt">void</span> <span class="nf">fragment</span><span class="p">()</span> <span class="p">{</span>
<span class="kt">int</span> <span class="n">c</span> <span class="o">=</span> <span class="n">color_index</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">texture</span><span class="p">(</span><span class="n">decal_map</span><span class="p">,</span> <span class="n">UV</span><span class="p">).</span><span class="n">r</span> <span class="o">></span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">)</span> <span class="p">{</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">color_index2</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">vec4</span> <span class="n">albedo_tex</span> <span class="o">=</span> <span class="n">texelFetch</span><span class="p">(</span><span class="n">texture_albedo</span><span class="p">,</span> <span class="kt">ivec2</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="mi">0</span><span class="p">);</span>
<span class="n">ALBEDO</span> <span class="o">=</span> <span class="n">albedo_tex</span><span class="p">.</span><span class="n">rgb</span><span class="p">;</span>
<span class="p">}</span></code></pre></figure>
<p>That’s it! It works! I hope this page helped you!</p>
<p><img src="/assets/img/isoputt_ball_trail_mountain.gif" alt="isoputt_ball_trail_mountain" title="Mountain Ball Trail Gif" /></p>JasonI’ve added a ball trail to IsoPutt. It serves two purposes: one, it’s like how in Super Meat Boy you can see all of your failed attempts. In IsoPutt, the ball trail doesnt reset when you fail the level, so you see all the lines you’ve made.Starting Isoputt2020-04-11T17:11:12+00:002020-04-11T17:11:12+00:00https://nightowl.games/blog/isoputt/starting-isoputt<p>I began working on Isoputt in December 2019 when I was inspired by <a href="https://www.kenney.nl/assets/minigolf-kit">KennyNL’s Minigolf</a> asset pack. I thought that I could use these tiles to make a simple game in a week. I placed some of Kenny’s tiles into the godot editor, and started trying to get the ball physics right. I started with a simple rigid body sphere. 5 months later and I’m still working on the same project.</p>
<p><img src="/assets/img/minigolf.gif" alt="minigolf" title="Minigolf Gif" /><br />
<em>My initial physics-prototyping level.</em></p>
<p>Turns out designing compelling content is hard.</p>
<p>The experience of making this game has changed my outlook on game dev completely. I now view level creation as the primary mode of game development; where systems and implementations merely serve as a scaffold upon which to build artistry. Before, I was stuck in the programmer focused mindset, constantly trying to create some perfect system which would generate fun. I now see game design differently; I’m trying to create a system where a single hour of my time can generate 5 minutes of entertainment for thousands of people. That’s the equation. Thats the goal.</p>
<p>Turns out fun is elusive. I had many failed level designs. After making the following level, I deleted all the levels I had made with KennyNL’s tiles, and started from scratch. I had moved the bar higher, and none of my previous levels reached the threshold of quality that I now required.</p>
<p><img src="/assets/img/hyperput_cliffhanger.gif" alt="hyperputt-cliffhanger" title="Isoputt Gif" /><br />
<em>After much iteration</em></p>
<p>This is what growth is. Deleting the old and starting again. After much more iteration, I began to form an effective design process. Now, I usually have a clear vision in mind before I begin. I have a notebook where I draw the simplest minigolf levels imaginable, and I slowly add complexity to the simplicity. Ideally, this allows me to fail on paper, instead of at my computer.</p>
<p><img src="/assets/img/hyperputt_showreel2.gif" alt="tee" title="Isoputt Gif" />
<em>Less is more</em></p>
<p>Simplicity is the key here. Less is more. I’ve spent countless hours adding more and more things to a bad level trying to make it fun, but it feels so impossible when you have a bad foundation. The good stuff forms when I have a good basis, and then I start removing the shots that I do not want to allow players to make. Finally, I focus on esthetics, adding small details in order to spice up the look of the game.</p>
<p><img src="/assets/img/isoputt_twins.gif" alt="twins" title="Isoputt Gif" />
<em>Details come last</em></p>
<p>My basic formula is this: 5 shots for a beginner player, 3 for an intermediate and 2 for an expert. Once I have a level that requires approximately that amount of shots, I iterate on the look of it. I’m still developing my esthetic style. But I have <a href="https://twitter.com/ODPomery">lots</a> of <a href="https://twitter.com/Sir_carma">inspiration</a> on twitter.</p>
<p>If your interested in Isoputt, please consider following me on <a href="https://twitter.com/00jknight">twitter</a>, subscribing to my email list, or donating on <a href="https://ko-fi.com/00jknight">kofi</a>. I’m always interested in feedback.</p>JasonI began working on Isoputt in December 2019 when I was inspired by KennyNL’s Minigolf asset pack. I thought that I could use these tiles to make a simple game in a week. I placed some of Kenny’s tiles into the godot editor, and started trying to get the ball physics right. I started with a simple rigid body sphere. 5 months later and I’m still working on the same project.Custom Character Controller in Unity2020-04-11T17:11:11+00:002020-04-11T17:11:11+00:00https://nightowl.games/blog/unity/custom-character-controller-in-unity<p>I’ve written countless character controllers in unity and have iterated on the velocity and drag many times. The following is the most basic character controller there is. There’s no max speed and it has a simplistic drag model.</p>
<h1 id="the-basic-algorithm-of-a-kinematic-character-controller-is-this">The basic algorithm of a kinematic character controller is this:</h1>
<figure class="highlight"><pre><code class="language-csharp" data-lang="csharp"><span class="n">Vector3</span> <span class="n">position</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">friction</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">acceleration</span><span class="p">;</span>
<span class="k">void</span> <span class="nf">Update</span><span class="p">()</span>
<span class="p">{</span>
<span class="n">velocity</span> <span class="p">+=</span> <span class="nf">GetInput</span><span class="p">()*</span><span class="n">acceleration</span> <span class="p">*</span> <span class="n">Time</span><span class="p">.</span><span class="n">deltaTime</span><span class="p">;</span>
<span class="n">position</span> <span class="p">+=</span> <span class="n">velocity</span> <span class="p">*</span> <span class="n">Time</span><span class="p">.</span><span class="n">deltaTime</span><span class="p">;</span>
<span class="n">velocity</span> <span class="p">-=</span> <span class="n">friction</span> <span class="p">*</span> <span class="n">Time</span><span class="p">.</span><span class="n">deltaTime</span> <span class="p">*</span> <span class="n">velocity</span><span class="p">;</span>
<span class="p">}</span></code></pre></figure>
<p>Implement that and you can start moving about the environment and play with the acceleration and friction. Max speed and acceleration curves are simple extensions. I dunno, something about a little cube running on top of another cube with the right friction and acceleration looks and feels so good to me.</p>
<p>The problem here, is that you’ll run through walls without any knowledge of their presence.</p>
<p>Enter collision detection, or rather, collision resolution.</p>
<h1 id="a-basic-collision-resolving-algorithm-looks-like-this">A basic collision resolving algorithm looks like this:</h1>
<figure class="highlight"><pre><code class="language-csharp" data-lang="csharp"><span class="k">void</span> <span class="nf">FixedUpdate</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">// check collisions</span>
<span class="kt">int</span> <span class="n">numOverlaps</span> <span class="p">=</span> <span class="n">Physics</span><span class="p">.</span><span class="nf">OverlapBoxNonAlloc</span><span class="p">(</span><span class="n">m_transform</span><span class="p">.</span><span class="n">position</span><span class="p">,</span> <span class="n">m_halfExtents</span><span class="p">,</span> <span class="n">m_colliders</span><span class="p">,</span> <span class="n">m_rigidBody</span><span class="p">.</span><span class="n">rotation</span><span class="p">,</span> <span class="n">layerMask</span><span class="p">,</span> <span class="n">QueryTriggerInteraction</span><span class="p">.</span><span class="n">UseGlobal</span><span class="p">);</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p"><</span> <span class="n">numOverlaps</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span> <span class="p">{</span>
<span class="n">Vector3</span> <span class="n">direction</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">distance</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">Physics</span><span class="p">.</span><span class="nf">ComputePenetration</span><span class="p">(</span><span class="n">m_boxCollider</span><span class="p">,</span> <span class="n">m_transform</span><span class="p">.</span><span class="n">position</span><span class="p">,</span> <span class="n">m_transform</span><span class="p">.</span><span class="n">rotation</span><span class="p">,</span> <span class="n">m_colliders</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">m_colliders</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">transform</span><span class="p">.</span><span class="n">position</span><span class="p">,</span> <span class="n">m_colliders</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">transform</span><span class="p">.</span><span class="n">rotation</span><span class="p">,</span> <span class="k">out</span> <span class="n">direction</span><span class="p">,</span> <span class="k">out</span> <span class="n">distance</span><span class="p">))</span>
<span class="p">{</span>
<span class="n">Vector3</span> <span class="n">penetrationVector</span> <span class="p">=</span> <span class="n">direction</span><span class="p">*</span><span class="n">distance</span><span class="p">;</span>
<span class="n">Vector3</span> <span class="n">velocityProjected</span> <span class="p">=</span> <span class="n">Vector3</span><span class="p">.</span><span class="nf">Project</span><span class="p">(</span><span class="n">velocity</span><span class="p">,</span> <span class="p">-</span><span class="n">direction</span><span class="p">);</span>
<span class="n">m_transform</span><span class="p">.</span><span class="n">position</span> <span class="p">=</span> <span class="n">m_transform</span><span class="p">.</span><span class="n">position</span> <span class="p">+</span> <span class="n">penetrationVector</span><span class="p">;</span>
<span class="n">velocity</span> <span class="p">-=</span> <span class="n">velocityProjected</span><span class="p">;</span>
<span class="n">Debug</span><span class="p">.</span><span class="nf">Log</span><span class="p">(</span><span class="s">"OnCollisionEnter with "</span> <span class="p">+</span> <span class="n">m_colliders</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">gameObject</span><span class="p">.</span><span class="n">name</span> <span class="p">+</span> <span class="s">" penetration vector: "</span> <span class="p">+</span> <span class="n">penetrationVector</span> <span class="p">+</span> <span class="s">" projected vector: "</span> <span class="p">+</span> <span class="n">velocityProjected</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">else</span>
<span class="p">{</span>
<span class="n">Debug</span><span class="p">.</span><span class="nf">Log</span><span class="p">(</span><span class="s">"OnCollision Enter with "</span> <span class="p">+</span> <span class="n">m_colliders</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">gameObject</span><span class="p">.</span><span class="n">name</span> <span class="p">+</span> <span class="s">" no penetration"</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>The only really interesting element here is that you must set the velocity to zero along the collision vector. We do that by using Vector3.Project to find what component of velocity needs to be eliminated. This makes it so when you hit the ground, for example, the -y element of velocity is eliminated.</p>JasonI’ve written countless character controllers in unity and have iterated on the velocity and drag many times. The following is the most basic character controller there is. There’s no max speed and it has a simplistic drag model.