Tower Defense page: distance fields

One reason I prefer having web pages instead of videos or academic papers is that they're easy to update over time. I'm still updating pages I wrote over 20 years ago.

Yesterday I was reviewing the Tower Defense page (2014) and decided the gap between the diagram at the top:

Tower Defense page: introductory diagram

Labels: ,

Touch events on the pathfinding pages, part 2

I've been avoiding mapgen4 for a few weeks now. I ran into a really annoying bug that frustrated me enough that I wanted to give up. I still need to get back to it, but I'm “productively procrastinating”. Instead of sitting around being frustrated, I'm getting other things done: map generation on a sphere, a procedural face generator, and adding touch event support to my pathfinding pages.

When I wrote the A* tutorial, I didn't consider touch events at all. The traffic I got from mobile/tablet was small. I soon added some touch event handling, but it was incomplete. I figured I'd fix it later. Well, it's been four years now. It's time to fix it finally! (Update: I think my touch event handlers used to work, but I think they stopped working when Chrome 56 and Safari 11.1 changed event handlers to default to passive sometime in late 2017 / early 2018; see this and this)

Labels:

Map generation on a sphere, part 2

I spent 9 days working on map generation on a sphere. The first 2 days were geometry: points, triangles, polygons on a sphere. The next 7 days were maps:

  • elevation: mapgen4's noise approach looked ok, but I wasn't happy with it, so I switched to using plate tectonics, which looked nice
  • moisture: mapgen4's evaporation+wind+rainfall relied too much on a grid, so I wasn't able to use it; I ended up assigning biomes randomly
  • rivers: mapgen4's binary tree forest works on graphs, and worked great on a sphere
  • rendering: mapgen4's elevation rendering uses outlines and a custom projection which are designed for flat maps, and I wrote a new simple renderer

I wrote up my notes and am going back to working on mapgen4. I'm also putting all the (messy) code up on github.

Labels: , ,

Map generation on a sphere, part 1

I ran into a showstopper with mapgen4. It unpredictably "freezes" in the sense that inputs have no effect, even though both the map generation and rendering are running. I'm taking a break from that for ProcGen 2018 - 9 days to work on a procedural generation project.

I decided to play with procedural map generation on a sphere. I've long wanted to try generating entire planets instead of flat maps. See this project and this project to get an idea of what I'm wanting to learn how to do.

Labels: , , ,

Mapgen4: river representation

I've reached the point in mapgen4 development when I just want it to be over. So I'm cutting all remaining features and am focusing on finishing. Most of the remaining work isn't interesting enough to blog about so I'm going to describe the river representation I started using last year. It's worked very well for the needs of this project. Take a look at the rivers in this screenshot:

Rivers are binary trees

Labels: , ,

Mapgen4: outlines

Usually when I make a map generator I focus on the underlying algorithms and data. The data will be used by a game project, so the visuals are meant to be informative and not necessarily pretty. For this project I wanted to make the output pretty. I previously blogged about the unusual projection I'm using for the graphics. It allows you to see the rivers and coastlines top-down but see the mountains from the side. There are two other interesting graphical tricks I'm using to make the maps prettier.

Non-photorealistic shading

Labels: , ,

Mapgen4: bug fixes

The past week has been troubleshooting. The biggest problem was water pouring out of my house's electrical panel, but that's not what this blog post is about. I've been working on bugs in the map generator. There are some bugs I've been putting off because I didn't understand them.

The first problem was that rivers haven't been reaching the ocean. This has been a problem for a while, but I didn't want to fix it until I stopped changing the algorithm. I'm mostly happy with the river algorithm now so I decided to investigate. One thing that the Game AI Summit at GDC emphasizes is building debug visualizations; I'm a fan of that technique and tried it here:

Debugging coastal and river triangles

Labels: , ,

Mapgen4: oblique projection

One of my goals for mapgen4 was to explore non-realistic approaches, both to map generation and to map rendering. I wanted to explain the unusual projection I'm using.

For procedurally generated wilderness maps, it's very common to see a top-down view, often using colors to represent height or biomes:

Top-down view

It's simple to implement, especially in 2D, and it shows the areas of the map, including coastlines and rivers and lakes. You can see where the mountains are, but only because of the color and lighting.

Labels: , , ,

Mapgen4: rainfall

Last time I rewrote elevation painting. The next thing I wanted to work on was biomes. I'm not actually even sure I need them, but I wanted to try a variant of an approach I had tried in the 1990s and again in 2009:

Rain cycle

Labels: , ,

Mapgen4: elevation painting, revisited

A few weeks ago I described elevation painting for mapgen4. The idea was to let the user paint areas for mountains, valleys, and oceans, and then use distance fields to interpolate between the areas. After some user testing, I decided to abandon this approach.

When I originally implemented it, I painted mountains and valleys on a minimap. On the main map, I used distance fields to transition between the mountain and valley areas.

Elevation constraints (red and green), and resulting elevation

Labels: , ,

Mapgen4: river appearance

Earlier this week I moved all the heavy computation into its own thread. The rest of the week I wanted to work on rivers. The last time I worked on rivers, I replaced the slow CPU-based renderer with a fast GPU-based renderer. However, the output was ugly:

Jagged rivers from my previous river renderer
Jagged rivers

Labels: , , ,

Mapgen4: threads

Today I am making mapgen4 multithreaded. I'm trying to blog more often, so I'm putting my notes here instead of keeping them to myself.

Multithreaded Javascript? Yes, with Web Workers. I've known about these for a while but I've never used them until now. The basics:

  • Web Workers are more like separate processes rather than threads. They have their own memory. Some browsers support shared memory but it's not something I can count on right now. Introduction and reference.
  • Workers can communicate by sending a message to another worker. The message is a data-only object (no functions or resources). Details.
  • You can also transfer data to another worker, if it's a contiguous array. This is particularly useful for large arrays of numbers such as WebGL data or, in my case, mapgen4 data including the Voronoi mesh. It avoids a copy.

I find that I learn best by reading a little bit and then playing with something. So I decided to make a simple worker and try it out.

Labels: ,

Mapgen4: elevation painting

Last week I described the plan for elevation in mapgen4. The goal was to allow painting mountains, valleys, and oceans on the map. I used a jittered grid to pick positions for mountain peaks, and then implemented three distance fields:

  1. Distance from closest mountain peak. I use this to create distinct mountains instead of noise-based continuous mountains.
  2. Distance from coastline. On the ocean side, I use this to make shallow water near the coast. On the land side, I use this to keep mountains away from coasts. (not sure I should)
  3. Distance from mountain ranges. I use this to make a gradual drop from mountains to valleys.

I continued optimizing. The main map regeneration is now around 400ms for the one-time initialization and 120ms to regenerate the map while painting. I implemented a simple paint interface, with small/medium/large brushes and ocean/valley/mountain tools. It works on touch devices too, and it feels pretty good on a tablet.

Example painted map
Map painting

Labels: , ,

Mapgen4: elevation

Last week I posted about mapgen4, the procedural map generation project I'm working on this summer. I had been trying to limit the scope of the project, and also shifting my thinking from maps for a game to maps that look pretty. I've been spending a lot of time optimizing and restructuring the code for this. It occurred to me that I'm approaching elevation all wrong.

Labels: , , ,

Mapgen4: goals

I've been posting on Twitter but was reminded that I should be posting more to my blog. I don't want Twitter to be the only place to read about what I'm doing.

Screenshot of a procedurally generated map

Back in 2010 I had written an influential article about map generation, making maps specifically designed for one game. I made a Flash demo called mapgen2. At the time I didn't emphasize enough how each of the layers of the process could be different. Many people took the recipe as the way to do it, even though the recipe was specifically for the needs of that one game. In mapgen2 the rivers depended on elevation which depended on coastlines. But it could've been coastlines depended on elevation which depended on rivers. There are lots of different ways to glue the pieces together. In 2017, I wanted to go back and try a different recipe with the same ingredients (Voronoi, elevation, rivers, biomes, etc.). For a while I thought the new project, mapgen4, will do all the things! I experimented with lots of things but the task seemed overwhelming. I decided to step back and limit my scope. I rebuilt the same algorithms with the new framework (HTML5, and more efficient data structures), and launched that as an HTML5 version of mapgen2. Then I put everything on hold.

Labels: , , ,

Delaunator guide

I decided that I should work on a new tutorial this summer. What better than to take all the map experiments I worked on last summer, and turn them into a procedural voronoi-map generation tutorial? As I worked on the outline and some of the initial diagrams I realized this was going to be big: poisson disc, delaunay, voronoi, elevation, climate, biomes, oceans, rivers, towns, roads, natural resourcess, and so much more. And starting with something big often leads to failure. So I needed something smaller.

I decided that I should start with just the structural part: Delaunay and Voronoi, and how they can be used for game maps: poisson disc, jitted grid, delaunay, voronoi, graph properties, neighbors, traversal, lookups, iterators, ghost elements, centroid vs incenter vs circumcenter, rounded regions, subdivision, and more. While working on the outline for this I realized again that it was going to be big. And starting with something big often leads to failure. So I needed something even smaller.

Labels: , ,

Modifying spelling

Back in 2016 I started a mini-project to play with neural networks. I concluded that the approach I used didn't give me enough control over the procedural generation. I wanted to revisit the project and focus on word manipulation (the problem I wanted to solve) instead of neural networks (the tool I was learning). Over the past week I read a lot, played with some code, and learned more about English. I tried modifying the spelling of existing words. Example:

Four skore and seven yirz ago our fatherz brougt forth on this kontinent, a noo nation, konceived in Liberty, and dedikated too the propozition that all men are kreated ekual.

I wrote up the results, including an interactive demo and instructions for compiling and using the software and dataset I found. I don't know what I will do with this, but it was interesting and I hope it's useful to someone.

Labels: ,

Thoughts on explorable explanations

Conversations with several people over the past few months have led me to wonder about the purpose of the interactive diagrams in my articles. I see lots of interactive diagrams but most are not integrated into text. The interactive diagrams are cool. They make me feel like a pioneer exploring a new medium. They're exciting! I like making them! But … that doesn't mean I should be making them.

Labels: ,

April updates: hex grid guide, load time

The past 7 weeks I've been working on my guide to hexagonal grids. Over the years I accumulated a long list of TO DO items on my Trello page, and I finally went through that list and implemented lots of them. Two blog posts ago I described rewriting it from an imperative style to a declarative style, and switching from manual to automatic dependency tracking. In the last blog post I described many of the improvements I made to the diagrams, including several new ones.

As I've added to the page over the years, it's become slower and slower. The main problem is the page loading time. The HTML loads, then the Javascript loads, then the Javascript runs to create the diagrams, sample code, and some of the text of the page. If you follow a link to a specific section, the browser jumps to that section, but then the layout changes as more content is added to the page by the Javascript. This is an unpleasant experience.

Labels:

April updates: hex grid guide, iteration

One of my goals this year is to iterate more. I have a tendency to "launch" a page and then move on to the next topic. Instead, for the first few months of the year I decided I would go through my existing pages to:

  1. figure out how the content could be better (explanations, diagrams, user interface, little details)
  2. figure out how the tech could be better (load time, animation code, diagramming code, code structure)

Over the past 6 weeks I've been working on my guide to hexagonal grids. I first worked on the tech, switching from an imperative style to a declarative style, and also switching to a library with automatic dependency tracking. Then I worked on content updates, some of which I described in my previous blog post. In the past week I've been going through my list on trello and implementing more content updates:

Labels: ,

April updates: hex grid guide

“We can rebuild it—we have the technology.”

Over the past few weeks I've been reimplementing my guide to hexagonal grids. I'm generally not a fan of rewrites that have no end-user benefits but there are lots of improvements I want to make to the page, and the convoluted code was making it harder to make the changes I wanted. I ended up spending 60 hours on this, reduced the number of lines of code from 2400 to 1400, and reduced the total Javascript sent to the browser (gzipped) from 85k to 54k. A large part of this was rewriting the diagrams to use Vue.js instead of D3.js. As much as I love D3, there's not much on the hexagon guide that benefits from it, and I ended up using it as a nicer JQuery. In a previous blog post I described wanting dependency tracking. That's why Vue.js worked well for this page. I think React would've been a reasonable choice as well, but on this page Vue fit my needs better.

Labels: ,

March updates: lots of small things

One of my goals this year is to publish in smaller chunks instead of trying to do everything in "one big launch". This is what I've been working on over the past few weeks:

Using Vue with Canvas

In my last post I had said I was trying out some libraries to automatically track dependencies for me. The one I'm playing with right now is Vue.js. It can track dependencies between data and the HTML/SVG elements. For example if I write SVG:

<circle :cx="center.x" :cy="center.y" r="30"/>

and I have it hooked up to data:

data: {
  center: {x: 100, y: 150}
}

then Vue will remember that the circle depends on those x and y properties. If I modify the data with center.x = 75; Vue will detect this and update the circle. This is quite convenient!

Labels: , ,

How I implement my interactive diagrams, part 2

In the last post I described how I structure my interactive diagrams and gave some examples. In this post I'll describe what I'd like to do for future diagrams.

Flow diagram: controls → input → algorithm → output → visualization

When I started writing interactive diagrams I used d3.js, which is a great visualization toolkit. At the time I thought data visualization was the hardest part of what I was doing, and I needed a library for that. Since then, I've seen other libraries — React, Riot, Ember, Mithril, Intercooler, Marko, Vue, Aurelia, Ractive, Rax, Svelte, MobX, Moon, Dio, Etch, Hyperapp, S/Surplus, Preact, Polymer, lit-html, Elm, and many more — that have given me different ways to look at the problem. I've realized that the visualization isn't the hardest part of what I'm trying to do. There are two two big problems I want a library to help me with:

  • How do I update the visualization?
  • Which algorithms do I need to run again?

Labels: ,

How I implement my interactive diagrams, part 1

I have an interactive tutorial about making interactive tutorials, showing how I made my line drawing tutorial. I wanted to describe how I made interactive diagrams in several other pages. The flow is:

Flow diagram: controls → input → algorithm → output → visualization

The controls and the visualization are what the user sees. The input, algorithm, and output are running behind the scenes.

Labels:

Updating C++ code on the A* implementation page

In my articles I have to make a tradeoff between making things approachable and including all the little details you might need in a real project. I tend to err on the side of making things approachable, but that means people can get frustrated when trying to translate these topics into real code.

For my A* pages I have a “theory” page that describes the main concepts but doesn't go into detail with code, and then I have a separate “implementation” page that shows more of the code. I included some C++ code that is abstract and reusable, but I've been unhappy with it. I decided to simplify it a little bit:

  • I dropped the template parameter for SimpleGraph because it's only ever used with char.
  • Even though Location is a property of the Graph, the templated code for this is verbose and distracting so I made Location a separate template parameter.
  • I switched from unordered_set and unordered_map to set and map; in practice you might prefer a hash table or an array, depending on the location type, but it's hard to pick one of these that's best for all cases.
  • I dropped some of the reference and const parameters.
  • I switched from set.count() to set.find() to test membership.
  • I wrote out explicit types instead of using auto.
  • I used std::xyz instead of using to omit the std:: namespace.

I have mixed feelings about most of these, and went back and forth a few times before deciding I'm overthinking it and just going with my gut.

Before:

template<typename Graph>
void dijkstra_search
  (const Graph& graph,
   typename Graph::Location start,
   typename Graph::Location goal,
   unordered_map<typename Graph::Location, typename Graph::Location>& came_from,
   unordered_map<typename Graph::Location, double>& cost_so_far)
{
  typedef typename Graph::Location Location;
  PriorityQueue<Location, double> frontier;
  frontier.put(start, 0);

  came_from[start] = start;
  cost_so_far[start] = 0;
  
  while (!frontier.empty()) {
    auto current = frontier.get();

    if (current == goal) {
      break;
    }

    for (auto& next : graph.neighbors(current)) {
      double new_cost = cost_so_far[current] + graph.cost(current, next);
      if (!cost_so_far.count(next) || new_cost < cost_so_far[next]) {
        cost_so_far[next] = new_cost;
        came_from[next] = current;
        frontier.put(next, new_cost);
      }
    }
  }
}

After:

template<typename Location, typename Graph>
void dijkstra_search
  (Graph graph,
   Location start,
   Location goal,
   /* out */ std::map<Location, Location>& came_from,
   /* out */ std::map<Location, double>& cost_so_far)
{
  PriorityQueue<Location, double> frontier;
  frontier.put(start, 0);

  came_from[start] = start;
  cost_so_far[start] = 0;
  
  while (!frontier.empty()) {
    Location current = frontier.get();

    if (current == goal) {
      break;
    }

    for (Location next : graph.neighbors(current)) {
      double new_cost = cost_so_far[current] + graph.cost(current, next);
      if (cost_so_far.find(next) == cost_so_far.end()
          || new_cost < cost_so_far[next]) {
        cost_so_far[next] = new_cost;
        came_from[next] = current;
        frontier.put(next, new_cost);
      }
    }
  }
}

The code isn't as fast but I hope it's a little easier to read. I added a section at the end with the faster code.

My goal is to show how the algorithms work, and not to write reusable library code. That's not what I'm good at. Whenever I try to show code, I get into these messes. I hope the code I have is a reasonable starting point for anyone who's writing their own instead of using a library.

This year I'm trying to iterate more and publish things more often. Take a look at the updated A* implementation page and let me know what you think. I'll add suggestions to the Trello page.

Labels:

Updating mapgen2

Back in 2010 when I wrote a procedural map generator based on Voronoi polygons instead of Perlin noise, the goal was to produce volcanic islands with a variety of monsters. I had started with Perlin-based height maps but got too much variety in the maps. I decided to use Perlin noise for deciding land vs water, but use other algorithms for everything else. I wanted the players to start on the beach and walk uphill, meeting other players who started elsewhere on the beach and were also walking uphill. I wanted to always produce islands with mountains in the center, so that the players on any beach would travel the same distance to get to the top. The maps were intentionally unrealistic but they were designed to satisfy the game design constraints. I made a Flash demo to accompany the article.

Labels:

Thinking about writing

I'm looking again at traffic to my site and how I write. A few years ago I realized I had a "ratchet" problem. My first articles were less polished, and I felt good publishing them. When I got better at writing, my newer articles were more polished, but I would hesitate to publish. I didn't want to publish until I got everything just right. And as a result I've been publishing less and less the last few years.

Why didn't I want to publish until I got everything right? I didn't want a traffic spike to occur and for lots of people to see my unpolished page:

Pageviews for my hexagonal grids page

The trouble is that all the metrics shown in the charts make those traffic spikes look very important. But this cumulative sum chart makes those traffic spikes look unimportant:

Cumulative pageviews for my hexagonal grid page

The spikes feel good. HEY I got on reddit! But they don't seem that important in the grand scheme of things. Most of my visitors come from day to day visits, not from the spikes. And that means my trying to make sure everything's polished before I put it up may be unnecessary. A friend of mine says I should treat my pages like wikipedia. The cumulative traffic graph supports that.

A few years ago I gave myself an “out” by having "x" pages that were less polished. They were blog entries. They were “fire and forget” pages that I would publish, unpolished, and not update. It worked. I published a lot more. I really liked publishing every few weeks instead of every few months.

The trouble was that it was too easy of an out. I ended up publishing almost everything as "x" pages the last two years, and I think it took away from my regular pages. In particular, the "x" pages let me get away with not iterating. I didn't update them after I posted. And they didn't get better. And then they got very little traffic.

If my pages are like wikipedia I should be putting up incomplete unpolished pages earlier and then refining them over time.

So this year I want to publish more often but more importantly iterate more after I publish. I can treat the early viewers as beta testers. It's the “launch early” philosophy from startup culture (minimum viable product, etc.). I tell other people to do this but I haven't been practicing what I preached.

Take a look at my Trello page to see the kinds of things I want to work on.

Labels:

What I did in 2017

What did I do in 2017? My plan, from my post a year ago, was:

  1. Help other people make interactive tutorials.
  2. Work on projects that lead to tutorials, instead of starting with the tutorials.

For the first goal:

I'm pretty happy with how that went.

For the second goal:

Although I did work on many small projects, I didn't work on any big projects. I have mixed feelings about this.

Other things:

  • I moved my site to a cheaper, faster web server
  • I converted my site to https, which was quite a bit more complicated than I thought it would be
  • I converted over 500 of my web pages from fixed width layouts to “responsive design”, and wrote an article about how it works ; it was a bit harder for my two column layouts
  • I made incremental improvements to most of my web pages (wording, diagrams, interaction, etc.)
  • I spent way too much time studying the color yellow
  • I made a tool to turn an image into a polygon mesh, originally intended for map generation (you'd paint a map in MS Paint and then turn it into polygons) but more fun as an art tool
  • I occasionally answered questions on stack overflow or reddit with interactive demos like this and this and this
  • I made a creepy procedural animation of a bacterial cell
  • I tracked these things on my trello page

What do I want to do in 2018?

  • I learned a lot from my map experiments this year, and I'd like to produce some useful tutorials and demos from them. Several techniques could be added to the existing map generator.
  • From working with others I was reminded how valuable iteration is. I want to go back to my older tutorials and update them.
  • I want to continue working with game developers on algorithms that could turn into future tutorials.
  • I want to write a tutorial on coordinate systems and cameras. I've tried this before but maybe the third time's the charm?
  • I'd like to write a few more tutorials about writing interactive tutorials.
  • I want to become faster at writing tutorials. Part of this is managing scope but part of it is using better libraries that let me reinvent less each time.

Website updates, part 2

In my last post I said I was going to update my website layout. I've been working on it for the past three weeks . The main change is responsive design: the page will adapt somewhat to the size of the browser window, including phone and tablet sizes.

website layout on various devices
Website layout on various devices

Of approximately 600 web pages, I have 514 converted to the new layout. The rest are either unimportant or impractical to convert.

To help myself understand the issues, I made some diagrams:

diagram showing the relationship between browser width and layout
Diagram showing the relationship between browser width and layout

I wrote up my notes about how I made the pages responsive.

I also fixed some glitches on my pages, and switched from downloaded fonts to system fonts (to make the page load faster).

For testing, Firefox's screenshotting tool and Chrome's screenshotting tool were both useful. I took screenshots before and after a change, and then reviewed the pages when screenshots changed.

# Firefox
/path/to/firefox \
    -screenshot \
    --window-size=$width,4000 "$url"

# Chrome
 /path/to/chrome \
     --headless --disable-gpu \
     --window-size=$width,4000 \
     --screenshot "$url"

Labels: ,