Introduce CellLayer and CellRegion abstractions.

The next step towards TS/RA2 tileset support (#5350) is to remove the assumption that map coordinates == cell coordinates (the isometric games have cell axes rotated by 45 degrees to the map).

The existing code required each trait that stored something for each cell to roll its own storage, usually via an inefficient 2D array.  The CellLayer class replaces this duplication with a wrapper around a 1D array, and which can transparently convert between cell and map coordinates (see [pchote:isometric](https://github.com/pchote/OpenRA/commits/isometric) for a sneak-peek at where this is going).

The next problem is enumerating over a set of cells on the screen / in the map / etc.  Looping over the x and y axes won't work, as they are rotated relative to the screen for isometric maps.  The CellRegion class introduces a simpler way of enumerating regions that will work for both perspectives.
A one-dimensional array is certainly nicer for performance - but I think most of it will get lost by exposing only ```IEnumerator<T>``` and not a concrete type. The runtime loses its ability to do array optimizations if it doesn't know it has an array to work with. We could mitigate this by creating an exposing an ```ArrayEnumerator<T>``` or something to that end.

Here's a test case you can run through [.NET Fiddle](https://dotnetfiddle.net/) or a quick console app.

Here's are some example runs from my machine:
```
Debug Build:
Array Time: 8.3ms
Enumerable Time: 16.7ms
Difference: 200%

Release Build:
Array Time: 2.4ms
Enumerable Time: 12.8ms
Difference: 537%
```

```C#
using System;
using System.Diagnostics;
using System.Collections.Generic;

public class Program
{
	public static void Main()
	{
		var input = new byte[1000000];
		new Random().NextBytes(input);

		// Warm up caches.
		Fnv1aHash(input);
		Fnv1aHash((IEnumerable<byte>)input);

		var sw = Stopwatch.StartNew();
		Fnv1aHash((IEnumerable<byte>)input);
		var enumerableTime = sw.Elapsed;
		sw.Restart();
		Fnv1aHash(input);
		var arrayTime = sw.Elapsed;
		Console.WriteLine("Array Time: {0:0.0ms}", arrayTime.TotalMilliseconds);
		Console.WriteLine("Enumerable Time: {0:0.0ms}", enumerableTime.TotalMilliseconds);
		Console.WriteLine("Difference: {0:0%}",
			enumerableTime.TotalMilliseconds / arrayTime.TotalMilliseconds);
	}

	static int Fnv1aHash(byte[] bytes)
	{
		int hash = unchecked((int)2166136261);
		foreach (var octet in bytes)
		{
			hash ^= octet;
			hash *= 16777619;
		}
		return hash;
	}

	static int Fnv1aHash(IEnumerable<byte> bytes)
	{
		int hash = unchecked((int)2166136261);
		foreach (var octet in bytes)
		{
			hash ^= octet;
			hash *= 16777619;
		}
		return hash;
	}
}
```