Sneaky (working title) is a simple, yet deep AI-centric stealth game. It emphasizes freedom of movement, strategy, and quick-thinking. There is no combat: it's purely a game of evasion. Influences include Jet Set Radio, Assassin's Creed, and the Metal Gear series.
Pictured is a randomly-generated test environment. Note the stereographic projection. The idea is to give the player a good view of (almost) the entire world at a glance, so they can easily pick a good path through to any location. Apart from that, the shots say little about visual style, because I'm putting off serious visual and audio work until the end (though I've an idea of where those aspects are headed).
It'll be free & open source. I'm using Unity Indie with C#. More on how I've implemented stuff, and the design as it stands later. A 20% completion figure must undoubtedly sounds ambitious considering how little is shown in those shots.
This is babby's first devlog, so please ask some questions if interested!
EDIT- moved this 1st post to 2nd post, tired of the ugly screenshot greeting my soon to be unpaying non-customers
Thanks for the encouraging words, guys!
What's going to be the players goal in the world? Where will they have to pick good paths to get to?
Abstractly, to complete a level the player will need to reach some number of locations, in any order they choose, within a certain time limit. As for what they must do at them thematically, it's undecided. Most likely collect objects. In any case, the locations will be clearly visible everywhere, via a column taller than the tallest building being projected upwards from each one.
I'm also thinking the player's position might be momentarily revealed upon reaching a location, so that evading enemies when there's lots of pressure is an integral part of the game, not just something players bring upon themselves by screwing up stealth.
The locations to be reached might be randomly placed, and initial enemy positions are certain to be. I don't want there to be much of a metagame to strategize in. Players must stay on their toes.
Pictured is an example of what's stored in the precomputed visibility data, in this case from the tile the player's on (a vertical tile in the second shot).
I need precomputed vis because the AI needs to reason about where to go, and their goal is to reach the player. To reach him, they need to first see him, so visibility information is crucial. Furthermore, it's utterly impossible to compute on-the-fly, because the NPCs need to make tens of thousands of visibility queries per second.
First some background. A map is defined by a 64x64 heightmap of integers. You might've guessed from the screenshots that the environments are urban. For my purposes, I've defined the environment to consist of tiles, both horizontal and vertical. There are exactly 64x64 ground tiles (the ground & roofs of buildings). A wall tile exists wherever there's a difference in height between two adjacent tiles in the heightmap: one wall tile per unit in the difference. The number of wall tiles varies depending on the map. Whether horizontal or vertical, all tiles have the same dimensions.
More background. It's (thankfully) not visible from the shots in the first post, but the world has a spherical topology: the left edge of the world is connected to the right, and the upper to the lower. Keep moving in one direction, and you'll end up back where you began. In fact, one of the edges is somewhere near the player in those shots (not sure where). Note that "edge" is kind of misleading, because there
are no edges in terms of gameplay, which is the point. It's just necessary to consider the edges for implementation purposes. In fact I just thought of a way to demonstrate the sphereness. Please click:
That's just the world as viewed from an orthographic camera top-down, tiled twice. It tiles infinitely. The shading is due to my fakeass precomputed AO (a story for another day).
Now, the visibility precalculation must answer the question
which tiles are fully visible from which tiles?. ("fully visible" means "an enemy can see from any point on tile
a to all points on tile
b"). To do this, it
1. Considers each tile in turn.
2. Considers tiles that might be visible from that
1.
3. Tests visibility from the tile given by
1 to each tile in
2.
Trouble is it's slow. Takes ~20 seconds per map on my Core i5 750. Not too surprising: with building height limited to 9 tiles, there are up to ~80,000 tiles. At the moment, there's still some room for optimization in
2 and (to a lesser extent)
3, while quite a bit is filled already. The whole thing's pretty easy to parallelize, because it can happen from
1, with no sharing of mutable data between threads. Not gonna go there unless I get desperate though.
Optimizations of
2 so far include considering each wall from the roof to the ground in that order. If a lower building is blocking my view of much of a taller one, there's a point above which I can see all of the taller building, and below which I can see none of it. Another optimization is considering only the tiles in front and to the sides. I can't very well see back through the wall behind me. Together, possibly in conjunction with things I don't recall, these doubled performance.
3 is implemented by raycasts. Originally I'd thought about doing something more sophisticated, but due in part to the "fully visible" requirement mentioned above, it got hideously complicated very quickly. Also, looking at the results of
this comparative roguelike FOV algorithm study, the performance of techniques other than simple raycasting isn't very impressive.
To start with, I used Unity's own raycast function. It worked for the case where a ray starts & ends inside the logical world boundaries. To make it account for the world's topology, I needed to rearrange the chunks making up the world many times throughout visibility precalculation.
Yet more background. The world consists of 8x8 chunks of 8x8 ground tiles each. Each chunk is a collider and a mesh. This is necessary because of the vertex limit per mesh, and so that the player can see across the world. Basically, the player is always near the center of the world, and when they get far enough from it, a new world center is selected, and everything's invisibly rearranged to account for that (Dark City anyone?), as though the world is a giant omnidirectional treadmill. Unbelievably, this actually works now, for both the world chunks, and for objects at arbitrary positions.
Doing this rearranging as necessary for vis precalc didn't immediately work. Rather than trying to figure out why, it was expedient to just write my own heightmap raycast algorithm, because I'd wanted some effects showing where the enemies can see anyway (which also must account for the world's topology), and because any solution would've complicated the rest of the game. Was tricky getting it working perfectly, but the results are worth it. No more complexity needed for vis precalc, and the performance thereof doubled again (Unity's raycast is general-purpose; I had much domain knowledge to use).
Sadly, the quadrupling of performance is what
got me to 20 seconds.
Being that this is precalculation, storage and retrieval are important considerations. Visibility is stored as a hash map from each tile to a unique list of tiles. All lists of tiles are stored densely in a big list (implemented as an array). When serialized to disk, it all comes to about 1.5 MB per map. One interesting thing here is that info about entire walls is stored as a start (highest tile visible on wall) and a count (total no. tiles visible on wall), cutting down on the necessary number of entries in the big list. Another is that the all info about a ground tile & its associated walls (x pos, y pos, wall start, wall end, ground visible) is stored in 8 bytes, and it's only stored if something's visible there. Suffice to say, the big list isn't getting more dense/less big anytime soon.
That concludes this magnificently verbose entry. Please give me your feedback, both on the game, and on the devlog so far. Next time I'm going to ramble on about controls & character controllers. There'll be a video! And I promise it won't be as lengthy!