Making 23040000 linecasts per second might be too taxing for the CPU

Started by
10 comments, last by serumas 10 years, 3 months ago

Hello

I'm planning to make a 2D top-down heist game (somewhat similar to Monaco, only you control more than one person at a time) in Unity, with levels consisting of squares.

I made a rough estimate of the size of a level (about 160x160 = 25600 tiles), and of the number of human or computer controlled characters in it at any given time (about 15). Every character will have to calculate its own LoS. Every frame, every character will have to make a linecast to every square, to check whether it's visible or not (if a square is invisible, it will appear a little darker). However, 160x160x15x60 = 23040000 linecasts per second seems a little CPU intensive, therefore I have thought of some solutions:

  • Check line of sight once per 1/2 second, instead of once per frame (divides amount of linecasts by 30 (assuming fps=60), bringing it back to 23040000/30 = 768000 linecasts per second (still a lot).
  • Don't check in a 360° circle around every person, but only in a 90° or 180° cone. However, this has two disadvantages:
    • Assuming the character is standing in the center of the level, it will only divide the number of squares by about 2 to 4.
      • Of course, you can set a maximum viewing distance to the cone to further limit the number of linecasts, but that seems a little unrealistic.
    • Determining which tiles are part of the cone might even take more CPU power than making a linecast to every tile.
  • Before making a linecast to a tile, first check if another person on your team ("human" is a team, and so is "computer") has vision on that tile. There could be a 160x160 twodimensional array humanVisibleTiles, which holds as a bool every square and whether it's visible for the humans or not. Say, for example, that person1 (controlled by the player) has made a linecast to square (40, 117). When person1 has checked all 160x160 squares, he submits his visible tiles, after which the union of person1's visible tiles and humanVisibleTiles will be stored in humanVisibleTiles. Then, person2 comes along, and is about to check (40, 117), but first checks if it is already visible in humanVisibleTiles. It will return true, which means that person2 doesn't have to check that tile at all, thus preventing one linecast. Of course, this solution is only effective if one if clause is less CPU intensive than one linecast, but I'm almost sure that's the case.

I have read through some other threads about raycasting/linecasting and its CPU cost (this thread in particular), and found that not casting lines every frame but at other intervals (my first solution) is the most effective. However, I can imagine that a player peeking around a corner and getting shot by a guard because his LoS updates only twice a second may get frustrated.

I'd like to get this part of the game development tackled, because it's a quite essential part of it. Monaco (and other 2D top-down games) have proven to handle this without demanding too much CPU, but then, Monaco doesn't cast a line to every tile every frame, but instead probably uses an algorithm similar to the one discribed here.

Do you have any tips on performance optimization? Thanks in advance!

Pintrix

Advertisement

Watching a gameplay video of Monaco, I don't see the same light-cone style lights for the guards. There's no reason to think that their vision is horribly complicated. It's very likely a ray-cast collision detection from every guard to every player character (that's O(guards * players) complexity). If the ray-cast comes back as a hit on the player, it's then some distance check to see if the player is in range. Before even sending the ray check, it'd be a dot-product to check if the player is "in-front" of the character and thus even worth doing the ray check or not.

If you're doing this on a tile grid as you say, use something like http://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm to trace out a check of only the tiles directly between you and the player, the average cost of tiles checked will be proportional to the average distance of a guard to the walls of his room, since that algorithm marches along the line one tile at a time.

Don't check in a 360° circle around every person, but only in a 90° or 180° cone. However, this has two disadvantages:

man, I wish I could see in a 360deg circle all the time smile.png. Why not limit the vision cone for enemies? unless these enemies are all 360deg motion detectors, for gameplay reasons you'll want to limit the vision cone. For the players, see below.

Determining which tiles are part of the cone might even take more CPU power than making a linecast to every tile.

Highly unlikely. In 2d, it'd be 2 ray casts to find the edges of the cone, from which you could then flood-fill, bredth-first from the source to fill in the center. You'd reverse the process every time your flood-fill lighting into a wall tile to block off a possible edge of the shadow. If you did ray-casts to every tile, you'd have to start caching the result (which would in turn become like the floodfill, only more expensive) because the farthest away tile checks need to check all the same nearby tiles you're claiming need a separate ray cast. So don't re-do any work ray casting to every tile, just flood-fill outwards.

Given the numbers you quoted, that'd be storage for 160x160x15 3 state { visible, shadow for sure, didn't check because shadow walls blocked my floodfill } mappings that you flood filled in. And then a single 160x160 map that you accumulate all the other 15 maps into, to get states like { full shadow, visible to some enemy, visible to some player, visible to enemies and players }. Yes it's still a LOT of tiles to deal with, but if you use my first comment on how to check enemy LOS, then you really only have to update the player's light maps quickly (hopefully something more like 1-4 of them) and you can space out the enemy lighting across several frames

Keeping an array of every single tile's visibility from every single other tile would be less than 80 MB (160^4 / 8), so just make a bool tileVisible[srcX][srcY][dstX][dstY] for now. If you actually need to hit a lower memory footprint later, worry about it later. For all you know, you'll change something that will make all of this coding irrelevant before launch anyways. And if you're targeting PC, you won't ever feel like 80 MB for map data is a waste.

I would just pre-calculate which cells are visible from each cell and store it as a bitmap representation in the level file and RLE encode it Then you wouldn't need to do any raycasts at all.

I can think of a lot of alternatives actually, but that one jumped out at me immediately :)

n!

Thank you, KulSeran, richardurich, and nfactorial, for your replies!

richardurich:

Yes, I'm planning on releasing this game solely on Windows and Mac, so 80MB of RAM won't be a problem at all, it is the CPU that bothers me (I'm not worrying about graphics, it is a graphically simple 2D game).

nfactorial:

This is in fact a very good solution, until the moment you open or close a door, or break down a wall (yes, there is an engineer with a drill, forgot to say that). As soon as that happens, all precalculations become obsolete, and every tile will have to recalculate their vision to all other tiles (and that would mean 160^4=65536000 calculations during the game sad.png ). If there is any other way to use your idea without this effect, please tell me, as I can't think of one.

KulSeran:

If you're doing this on a tile grid as you say, use something like http://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm to trace out a check of only the tiles directly between you and the player ...

I very much like that suggestion! Would it mean that every guard makes a LOS check to the players with a Bresenham line, wich immediately stops as soon as they encounter an obstruction such as a wall? Also, would this be faster than a Unity built-in raycast?

You'd reverse the process every time your flood-fill lighting into a wall tile to block off a possible edge of the shadow.

I'm not sure what you mean by reversing the process, and how that is going to help blocking off a possible edge of the shadow.

If you did ray-casts to every tile, you'd have to start caching the result (which would in turn become like the floodfill, only more expensive) because the farthest away tile checks need to check all the same nearby tiles you're claiming need a separate ray cast.

Why whould I have to cache the result map of the 160x160 raycast? Because it would be hard to access it every (few) frame(s)?

Why do the farthest away checks need to check all the same nearby tiles I'm claiming need a separate ray cast when I'm not using the floodfill method? I'm afraid I really don't get that.

Pintrix

You need a 3-DOP vs BVH or quad tree broad phase with a 3-DOP-to-tile narrow phase.

3 2D planes make a frustum that represents the enemies’ field-of-view.

Broad phase: All tiles are held in a quad tree or BVH. Simply traverse whichever structure you use without needlessly testing areas clearly not inside the frustum.

Narrow phase: Until you get to the root nodes in which you test tiles themselves.

Tiles not on the screen don’t need to be considered.

Remaining tiles are then ray-traced.

L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

Why do the farthest away checks need to check all the same nearby tiles I'm claiming need a separate ray cast when I'm not using the floodfill method? I'm afraid I really don't get that.

If you have a character in the center of your map, and you check the 8 tiles surrounding it, and the top 3 tiles are all walls then what? Why would you even bother checking the ~25% of the map in the triangle made from the top left corner of the map to top right to center? you already know that every single one of those ray checks has to fail, it's going to hit the same 3 wall tiles you already ray checked were on the north side of the character. That's ~6400 squares you can mark as "in shadow from character X" after doing those 3 ray casts finding walls, then two lines drawn to mark off the triangle. But each of those ray casts is "cheap" in that it nearly instantly finds a wall. The further away the wall, the more tiles you're re-checking with each ray cast.

Look in 1d:


XXXXX*----X

If * is your character, and you "ray cast" every single tile. The ray casts from 1 to 5 all do the same exact check, is 5 clear? no it's a wall. give up. All the ray casts from 7 to 11 say is 7 clear? ok. is 7 and 8 clear? ok. is 7 and 8 and 9 clear? ok. is 7 and 8 and 9 and 10 clear? ok. is 7 and 8 and 9 and 10 and 11 clear? no. 11's a wall.

Had you just flooded out from the *, 5's a wall stop. 7's clear, 8's clear, 9's clear, 10's clear, 11's a wall stop.

And now back to 2d, notice how, as you fill outward, you can start blocking off triangles of tiles that you no longer need to do ray casts to, because it's not possible to reach them due to already finding a wall. 18 of 81 tiles weren't checked in my diagram, or ~22%. And since I'm flood filling, there were only 8 ray casts to draw the 4 triangle cones for the shadows.


X???????X    X???????X    X???????X    X???????X
?X?????X?    ?X?????X?    ?X?????X?    -X?????X-
??X???X??    ??X???X??    X-X???X-?    X-X???X--
???XXX???    ??-XXX-??    XX-XXX--?    XX-XXX---
???-*-??? -> ??--*--?? -> ?---*---? -> ----*----
???---???    ??----XXX    ?-----XXX    ------XXX
?????????    ??-----X?    ?------X?    -------X?
?????????    ????????X    ?-XXX---X    --XXX---X
?????????    ?????????    ?X??X????    -X??X----

If you are a 2d tile based game, then yes, doing bresenham or just about any other tile operation will be much faster than using Unity's raycast function. In fact, if you're going for speed and are a 2d tile based game, I wouldn't use unity's raycast much at all. Unity's raycast function doesn't know that it doesn't need to check every single tile on the map (Unless maybe you were dynamically moving which tiles were on which layers, but at that point you would already know which tiles to check)

Also limited line of sight makes sense for when a level isn't brightly lit, or has fog, or just for hellishly long distances, so a cone isn't a bad idea, especially since it might be very frustrating for a player to get spotted by a guard is who very far away and off the screen.

Also limited line of sight makes sense for when a level isn't brightly lit, or has fog, or just for hellishly long distances, so a cone isn't a bad idea, especially since it might be very frustrating for a player to get spotted by a guard is who very far away and off the screen.

I agree with you. Maybe it's better to give the players (or at least the guards) a maximum viewing distance, to prevent such situations.

KulSeran:

Thanks, I understand the idea now! However, on your 2D example, I encountered a problem. I will be using * for the character, - for empty space, / for an unchecked tile, X for definitely shadow (checked), and O for a tile that is marked as part of the shadow line because of the wall between it and the character.

Given a level as shown in fig 1(without fog or other vision obstruction. Simply the level lay-out), I do not understand the way you cast the shadows behind the edges of a wall. fig 2 shows step 1 in the area scanning process. This step isn't hard to understand, with two lines following from the edges of the wall. However, I don't quite get the second step, where there are two shadow lines, appearing out of a wall tile. How did you decide what direction the shadow lines would be cast in? The same thing goes for the upper-left piece of wall (next to the original 3 tiles wide wall).


     ---------     O///////O     O///////O
     ---------     /O/////O/     /O/////O/
     ---------     //O///O//     //O///O//
     -X-XXX---     ///XXX///     //-XXX-//
     ----*----     ///-*-///     //--*--//
     ------X--     ///---///     //----XOO
     ---------     /////////     //-----O/
     --XXX----     /////////     ////////O
     ---------     /////////     /////////
     fig 1         fig 2         fig 3
     lay-out       step 1        step 2

The only thing I can think of is that you decided that for wall tiles that appear alone, like the two cases I mentioned, one or two shadow lines will be drawn, depending on where the wall is positioned relative to the character, as shown in the following table


011123334
F01123345
FF0123455
FFF024555
EEEE*6666
DDDCA8777
DDCBA9877
DCBBA9987
CBBBA9998

In area 0, only one shadow line will be drawn; to the north-west

In area 1, one shadow line will be drawn to the north-west, another to the north

In area 2, only one shadow line will be drawn; to the north

In area 3, one shadow line will be drawn to the north, another to the north-east

In area 4, only one shadow line will be drawn; to the north-east

In area 5, one shadow line will be drawn to the north-east, another to the east

In area 6, only one shadow line will be drawn; to the east

In area 7, one shadow line will be drawn to the east, another to the south-east

In area 8, only one shadow line will be drawn; to the south-east

In area 9, one shadow line will be drawn to the south-east, another to the south

In area A, only one shadow line will be drawn; to the south

In area B, one shadow line will be drawn to the south, another to the south-west

In area C, only one shadow line will be drawn; to the south-west

In area D, one shadow line will be drawn to the south-west; another to the west

In area E, only one shadow line will be drawn; to the west

In area F, one shadow line will be drawn to the west, another to the north-west

For wall tiles not appearing alone, but in together (like most walll tiles will), I suppose that as soon as you find an edge, you continue the Bresenham line between the character and the wall tile, until it exits the level boundaries?

Once again thanks for all your replies!

Pintrix

Yeah, sorry hard to draw in 2d... I'd say, for every tile you discover that is an X, do 4 ray-plane intersections in floating point. Send a ray from the player's center (eg <3, 20>) to the 4 tile corners (eg. for the tile <2, 2> that'd be <2.5, 2.5> <2.5, 1.5> <1.5, 1.5> <1.5, 2.5>). You'd end up with 4 lines that intersect at some edge cordinates like <1.95,0>, <1.45, 0> (assuming i can roughly math...) Round that off to the nearest integer tile centers <1, 0> and <2, 0>, and then do the bernstrum line drawing from <2,2> to <2,0> and <2,2> to <1,0> to draw the shadow edges.

This topic is closed to new replies.

Advertisement