[RELEASED] PixelSurface: Efficient Destructible 2D Terrain

My own project is a little different because I generate levels ahead of time in the editor with pixel destruction, and yeah that kind of feature might help out some pixel surface customers, to have the option to say, insert a source texture and have it pre-generate the level at edit time, and if your feeling like really pushing it, even let the user do some editing of the world in edit mode (I think ragepixel does this if I remember correctly) to add maybe shapes like you already can do at runtime with pixel surface, or even better, ā€œstampā€ in another textureā€¦

Like say you have two hills, and a big gap in the middle, and you want to use that as the game world basis, then you have a texture of a bridge, and you want to ā€œstampā€ a copy in the middle of those two hills. That would be cool! Iā€™ve thought about writing something like that myself, which really doesnā€™t seem to complicated in theory, just have a sprite that shows its positioning while you drag it where you want it, then click an editorscript button that takes the corner of it, gets the world coordinates under the corner, then basically adds the pixels over the existing world based on the offset of where the corner satā€¦ anyway I better shut up I could ā€œtheorizeā€ all day about this, but I wonā€™t be getting any work done haha.

Another unrelated note, I pushed my editorscript a little further and implemented that ā€œrolling hillā€ style generator I was talking about, here is a pic of it in play mode, and the editorscript which has a few settings to mess with:

The debug script I had up there while I mess around with testing an A* implementation Iā€™ve been writing, disregard that sillyness, haha. I might generalize it some more and hook it up to pixel surface, then I could share with you guys if your interested in that ā€œrolling hillsā€ thing at all. As it is right now, it is only set up to work with my own project, but yeah wouldnā€™t be too tricky to make it like the tunnel/island generator posted above, so it could use pixel surface.

Oh while Iā€™m thinking of it, if you do go to make pixelsurface generate pre-runtime, I found I had to generate and save materials (not the textures themselves though) in order for it to work. Otherwise it would lose connection between the generated textures and the actual scene objects when you went into play mode.

1 Like

Incidentally, I havenā€™t forgotten the request for a PixelSurface demo of simulating liquids.

Iā€™m currently reading this blog post, which looks like a pretty promising way to do it.

Incidentally, it also raises the topic of using a PixelSurface as a cellular automaton (or CA for short), which is something Iā€™ve thought about before. The only tricky bit here is that in a CA, itā€™s very important that you calculate the new state of all the cells based on the old state of all the cells ā€” you canā€™t just update them one at a time, because this allows information to propagate faster in some directions than in others (depending on the order in which you update the array).

In the case of a water simulation it may not matter, but for some kinds of CAs it could make a huge difference. I donā€™t know if thereā€™s a good general solution ā€” I can think of several ways to solve it, but different ones would perform better based on what fraction of the cells change state on each time step, etc.

Anyway, just wanted to let you all know that Iā€™m still meditating on this one, and I will definitely post a demo when I have something working!

1 Like

Iā€™ve implemented that CA approach to fluid simulation. I have mixed feelings about it: it does make a pretty decent fluid in most cases, but in the test where you make two connected buckets (like the below), it takes a long time for the fluid levels to equalize.

You can try the demo here. And hereā€™s the code:

/* Simple water/fluid Cellular Automaton (CA)
based on: http://w-shadow.com/blog/2009/09/01/simple-fluid-simulation/
*/

using UnityEngine;
using UnityEngine.Events;
using System.Collections.Generic;
using PixSurf;

[RequireComponent(typeof(PixelSurface))]
public class FluidCA : MonoBehaviour {
    #region Public Properties
    public bool running;

    #endregion
    //--------------------------------------------------------------------------------
    #region Private Properties
   
    // The mass of water at each cell in the grid.
    float[,] mass;
   
    // "new" mass, used only during simulation step (but kept around for efficiency)
    float[,] newMass;
   
    PixelSurface surf;
    int width, height;
   
    const float kMaxMass = 1.0f;        // The normal, unpressurized mass of fluid in a cell
    const float kMaxCompress = 0.05f;    // how much excess fluid a cell can have with just ONE full cell above
    const float kMinMass = 0.0001f;        // at what mass of fluid we consider a cell to be dry
    const float kMaxFlow = 1f;            // max units of water to move out of one block to another per timestep
    const float kMinFlow = 0.01f;
   
    #endregion
    //--------------------------------------------------------------------------------
    #region MonoBehaviour Events
    void Start() {
        surf = GetComponent<PixelSurface>();
        width = surf.totalWidth;
        height = surf.totalHeight;
        mass = new float[width, height];
        newMass = new float[width, height];
       
        Reset();
    }
   
    void Update() {
        // Update the water simulation.
        if (running) DoOneStep();
       
        // For testing: add water upon mouse button 0; with mouse button 1, draw/erase ground.
        if (Input.GetMouseButton(0)) {
            Vector2 pos;
            if (surf.PixelPosAtScreenPos(Input.mousePosition, out pos)) {
                AddFluid((int)(pos.x), (int)(pos.y), kMaxMass);
            }
        }

        if (Input.GetMouseButton(1)) {
            Vector2 pos;
            if (surf.PixelPosAtScreenPos(Input.mousePosition, out pos)) {
                bool erase = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
                surf.SetPixel((int)pos.x, (int)pos.y, erase ? Color.black : Color.yellow);
                ClearFluid((int)pos.x, (int)pos.y);
            }
        }
    }

    #endregion
    //--------------------------------------------------------------------------------
    #region Public Methods
   
    public void DoOneStep() {
        // Loop over the grid, updating newMass (which should already be a copy of mass)
        // with new values based on fluid flowing out of each cell.
        for (int y=0; y<height; y++) {
            for (int x=0; x<width; x++) {
               
                float remainingMass = mass[x, y];
                if (remainingMass <= 0) continue;    // ToDo: kMinMass?

                // Flow down into the block below.
                if (Available(x, y-1)) {
                    float massBelow = mass[x, y-1];
                    float flow = GetStableStateBottom(remainingMass + massBelow) - massBelow;
                    if (flow > kMinFlow) flow *= 0.5f;    // loads to smoother flow?
                    flow = Mathf.Clamp(flow, 0, Mathf.Min(kMaxFlow, remainingMass));
                    remainingMass -= flow;
                    newMass[x, y] -= flow;
                    newMass[x, y-1] += flow;
                }
               
                // Flow left.
                if (Available(x-1, y)) {
                    float flow = (mass[x, y] - mass[x-1, y]) / 4;
                    if (flow > kMinFlow) flow *= 0.5f;
                    flow = Mathf.Clamp(flow, 0, remainingMass);
                   
                    remainingMass -= flow;
                    newMass[x, y] -= flow;
                    newMass[x-1, y] += flow;
                }
               
                // Flow right.
                if (Available(x+1, y)) {
                    float flow = (mass[x, y] - mass[x+1, y]) / 4;
                    if (flow > kMinFlow) flow *= 0.5f;
                    flow = Mathf.Clamp(flow, 0, remainingMass);
                   
                    remainingMass -= flow;
                    newMass[x, y] -= flow;
                    newMass[x+1, y] += flow;
                }
               
                // Flow up (if compressed).
                if (Available(x, y+1)) {
                    float flow = mass[x, y] - GetStableStateBottom(mass[x, y] + mass[x, y+1]);
                    if (flow > kMinFlow) flow *= 0.5f;
                    flow = Mathf.Clamp(flow, 0, Mathf.Min(kMaxFlow, remainingMass));
                   
                    remainingMass -= flow;
                    newMass[x, y] -= flow;
                    newMass[x, y+1] += flow;
                }
               
                if (remainingMass < 0) {
                    Debug.LogError("Whoops!");
                }
                //                newMass[x, y] = remainingMass;
            }
        }
       
        // Copy newMass back into mass, now that all flows are accumulated,
        // and update the display at the same time.
        for (int y=0; y<height; y++) {
            for (int x=0; x<width; x++) {
                float m = newMass[x, y];
                if (mass[x, y] != m) {
                    mass[x, y] = m;
                    if (m < kMinMass) surf.SetPixel(x, y, Color.black);
                    else {
                        float a = m/(kMaxMass*2);
                        surf.SetPixel(x, y, new Color(a, a, 1));
                    }
                }
            }
        }
    }
   
    public void AddFluid(int x, int y, float amount) {
        mass[x, y] += amount;
        newMass[x, y] += amount;
    }
   
    public void ClearFluid(int x, int y) {
        mass[x, y] = newMass[x, y] = 0;
    }
   
    public void Reset() {
        for (int y=0; y<height; y++) {
            for (int x=0; x<width; x++) {
                mass[x, y] = newMass[x, y] = 0;
            }
        }
        surf.Clear(Color.black);
    }
   
    #endregion
    //--------------------------------------------------------------------------------
    #region Private Methods
   
/// <summary>
    /// Figure out whether the given pixel position is available for fluid to use.
    /// </summary>
    bool Available(int x, int y) {
        if (x < 0 || x >= width || y < 0 || y >= height) return false;
        Color c = surf.GetPixel(x, y);
        // Transparent or pixels are always OK.
        if (c == Color.black || c.a < 0.5f) return true;
        // As are pixels that look like water (i.e., have full blue).
        if (c.b > 0.99f) return true;
        // Otherwise, assume it's ground.
        return false;
    }
   
    /// <summary>
    /// Given a total amount of water, calculate how it should be split between
    /// two cells stacked vertically.  Return the amount of water that should be
    /// in the bottom cell in the stable state.
    /// </summary>
    /// <param name="totalMass"></param>
    /// <returns></returns>
    float GetStableStateBottom(float totalMass) {
        if (totalMass <= kMaxMass) {
            return kMaxMass;
        }
        if (totalMass < 2*kMaxMass + kMaxCompress) {
            return (kMaxMass*kMaxMass + totalMass*kMaxCompress) / (kMaxMass + kMaxCompress);
        }
        return (totalMass + kMaxCompress)/2;
    }
   
    #endregion
}

Iā€™m pondering a completely different approach to fluid simulation, where you group all the water pixels into connected regions (i.e. collections of pixels that are connected). Then you just find the highest pixel, and then find the lowest empty space next to any pixel in the group. And you simply teleport it there. Rinse and repeat.

I believe this will make water levels equalize out much more quickly, though the book-keeping to do this efficiently could be a bit thorny.

Anyway, in the meantime, hereā€™s one approach to fluid thatā€™s fun to play with!

3 Likes

Awesome, I see what your saying about the slow speed of it transferring. That is still awesome though, and I canā€™t wait to see how your other approach works.

Iā€™ve been testing A* like crazy and have written a super dumbed down algorithm which works to find a path from point a to point b, and it runs quick for ~100 pixel long paths, but really bogs down on say, ~8000 pixel long paths haha. If I manage to work some kinks out I think it might be a stable way to do AI pathfinding in this type of game. It isnā€™t fast enough now to be worth much but when I get to profiling and pinpointing slowdowns I bet I can get it going betterā€¦

Still got that physics object idea in the back of my head but havenā€™t taken a shot at that yet. That will be mighty tricky.

1 Like

Hey, I was looking around, the pixels, are they just gameobjects given virtual data that carry rect information, or do they actually hold individual sprite rendering objects on each?

from what I can understand, you hold a pixel from an individual texture inside of an array which collects each value in the array, and then you append this value to an individual pixel gameobject thats been pooled? so the only way to keep track of the pixels dynamic, or otherwise are the ones stored in this virtual array of point data (but not really physical art, just math representing rects of the x and y of each pixel in virtual space somewhere (like inside these arrays))?

Well, here is how I see it (I didnā€™t make it so canā€™t specify exactly) but the ā€œsurfaceā€ is essentially a big texture2d (although literally they are split into sections, each a quad) which you can do normal SetPixel() and GetPixel() methods through, and the surface allows you to also consider collisions (whether or not a pixel is ā€œsolidā€) and it also provides a way to have ā€œdynamic pixelsā€ which can move with physics around the environment.

I didnā€™t look super thoroughly through the code, but I almost guarantee it is pooled for the dynamic pixels, and they are not really their own graphical gameobject at all, they are a special type that only keeps data like color, velocity, and world position. Then when the physics engine detects one of these dynamic pixels has come to a stop, it is then added back to the overall world texture and ā€œsolidifiedā€ into the terrain. They donā€™t have a texture, as they are just a single pixel, and instead only have a color.

I hope I sort of answered some of your question?

EDIT: as another note, I once tried this exact sort of thing, with a pool of actual one pixel sized sprite objects, moving around in world space, and trying to achieve the same effect that way is a losing battle, as so much performance is lost doing all the enable/disabling of renderers and whatnot, plus just having thousands of extra gameobjects with renderers and yeah, to have each pixel be its own graphical representation and gameobject is basically a no-no within unity! It had to be at least 50 percent slower than my earliest tests doing this sort of thing years agoā€¦ food for thought I guess

@MD_Reptile 's description seems correct. Static pixels are just colors in a Texture2D, rendered to the screen via a Quad (and as he pointed out, the surface as a whole is divided into one or more quads, so you can adjust the balance between texture updates and draw calls).

Dynamic pixels are lightweight objects consisting of, in the base class, a color. You can make subclasses to hold more data. These exist in ā€œstacksā€ on each pixel location that has any dynamic pixels there. The topmost color on the stack is rendered to the texture, and thus to the screen.

Cheers,

  • Joe

Well, I am trying to compare this to other things ive read about. I cant fathom them using a ton of individual renderered pixels.

If I was to use A* pathfinding pro with this, would I be updating my path values based only on data stored in an array to account for change? I guess id be better off coding my own pathfinding which accounts for the array data (transparent blocks of regions of pixels within the surface quad?) to allow objects to detect their area (maybe raycast lines to hit each spot?? I dunno)

well im kinda confused but will try to figure it out

Iā€™ll tell you right now - I am testing a Manhattan A* (way simpler than any assets on the store) that simply takes point A and point B (two pixel locations in the world) and creates a path (as of now, not considering gravity, or jumping) and finds a ā€œgoodā€ path based on a heuristic - and I gotta say, trying to do that calculation on anything over a couple hundred pixels starts to be very performance intensive (at least on my i5 notebook) and can definitely not be used as it is in a game with worlds any larger than say, 256 x 256 effectively.

My solution (both that Iā€™ve thought of, and that Iā€™ve asked Dan Tabar from Data Realms about and he pointed me in this direction) that I think would solve that problem is to break the world up into sections, say 32x32 pixels or so, and have those act as the ā€œnodesā€ to your pathfinding, and in turn you have to determine whats an ā€œopenā€ or ā€œclosedā€ node based onā€¦well magic :smile:

In my head it goes like this:

  1. determine total count of pixels in a cell
    2a) if over a threshold, do a ray/line cast downward X amount until a hit (solid pixel) is detected
    3a) if a solid pixel is found in step 2 over the X threshold, then cast diagonally based on that collision (once a positive slope, once a negative slope) and determine just how ā€œopenā€ the cell is
    4a) if 2 and 3 have been satisfied, then the cell is marked open to the pathfinding graph
    2b) if it is under a threshold (very few pixels) then consider it clear, and move on

So this would mean pre-runtime you do this once for every cell in the world, finding all open cells, then at runtime, when changes happen to the world, mark those cells as needing an update for whether or not they are open cells still, and then have any agents following a path which crosses any updated cells recheck their path.

You wonā€™t be doing normal built in unity raycasting for this type of thing, but instead use something like Bresenhamā€™s line algorithm, based on which pixels are solid or not being ā€œcollisionsā€ with the cast.

Since agents wouldnā€™t always go inside of a node (if your character was say, 16x32 pixels tall) you would have to check that they just come close enough before continuing toward the next node.

PLUS, assuming your doing a ā€œsidescrollingā€ style game, or ā€œpocket tanksā€ style game, you need to consider gravity, and perhaps jumping, to make sure your paths are possible. This means adjusting your heuristic to account for impossible to reach jumping heights, unless you want to program your AI to just blow a hole anytime they cant jump over somethingā€¦

Edit: I havenā€™t totally given up on a per-pixel a* for big world sizes, the last thing Iā€™m going to try is offloading work to multithreading, and to determine if I can speed it up enough that way, and if it works for mobile. Then forget a higher resolution grid! But thatā€™ll be tough to get right, Iā€™ll report back how that turns out.

Yes, A* on a high-resolution grid is going to bog down. But there are lots of alternatives to A*ā€¦ everybody reaches for that because it produces an optimally short path in a reasonably fast time when you have no extra data that lets you do better (and assuming you have a permissible heuristic, which means, for example, no teleporters). But any of those assumptions may not apply in a particular game:

  • Do you really need the optimal path, or just a good path? Consider a greedy search, which (unless your map is a maze with lots of tricky dead ends) will probably produce a decent path, much faster.
  • Is there extra data you can use? For example, if youā€™re often path-finding to the same point (location of your base, nearest tree, whatever), then you can flood-fill a complete distance map, that gives you the distance from that point from every other reachable point. Then anybody can reach that goal with no path-finding at all: you simply step from where you are to whatever neighboring location is closest, and repeat.
  • And if your game has ways of teleporting, then A* isnā€™t guaranteed to find the shortest path anyway, so why are you using it?

And then, of course, even when A* is the right search algorithm, you have to decide what your locations and steps are. This is what @MD_Reptile is getting at with dividing the world into cells. You could also divide the world into polygons, and do path-finding from the vertices of the polygons. Either of these would be a dramatic speed-up over working at the pixel level.

But, all this does suggest a PixelSurface featureā€¦ maybe a ray-cast function should be built in? Or perhaps more generally, a function that you call with two points and a delegate, and it invokes that delegate for each pixel location along that line, or until you tell it to stop. So you could use that for ray-casting, or for modifying the pixels in some way.

1 Like

You got me thinking, I tried A* doing multithreading, and although it alleviates lag on the main thread, it just plain takes too long. So what about this - similar to your caves generator, and how it generates grass at the top area of terrain, you could do a similar method to label all ā€œwalkableā€ pixels, rather than using an a* grid (which considers all kinds of places you canā€™t possibly reach in a sidescroller) and instead just check heights along ā€œwalkableā€ lines, and if they are not too high to jump too, they are valid paths across, and then itā€™s a matter of connecting the paths, and anywhere that is an obstacle, destroy it, and wherever there is a gap, bridge it somehow (for my game I might try having the ai use the ā€œpixel sprayerā€ to create a little bridge). It would mean way less nodes for pathfinding to consider, and need basically a simple heuristic based on y height of the separated walkable areasā€¦

Is this making sense to anybody? :stuck_out_tongue:

Edit:
Like this -

Yeah, sounds good.

But pathfinding in a platformer is tricky even in a tile-based worldā€¦ with a pixel world, itā€™s going to be even harder. Super Mario Bros didnā€™t have any pathfinding at all; enemies just used very simple behaviors (e.g. walk until you reach a cliff or an obstacle, then turn around). All the fancy movements are left to the player.

I donā€™t know what game design youā€™re working on, but is it possible to alter it in such a way that you can dispense with the smart AI completely, and still have a fun game?

Just thought Iā€™d ask! :smile:

Iā€™m working on a shooter, with weapons like an rpg, rifles, and tools like a digging tool and pixel spraying tool, and the idea would be either A) decent ai who can find players who have dug deep into the level (like zig zagging through 8192 pixels) even when they have to travel through obstacles and bumps too large to jump by modifying the environment. Or B) online multiplayer. Forget ai altogether and focus on making it a deathmatch/ctf/attrition style gamemode, based on kills or objectives, with the ability to blow away parts of the enemy base, create obstacles to prevent enemies reaching your own baseā€¦ I could go on, but I guess the thing is, I halfway hope to let unet mature before I mess around with networking, as I donā€™t want to screw it up having problems that are possible bugs in unet, plus itā€™s hard to network this, so Iā€™ve kind of focused on a single player aspect instead, with the hopes of having the ai compare to the likes of Cortex Command (google a gameplay video or better yet grab a copy on steam - I canā€™t believe youā€™ve made this asset and havenā€™t heard of CC :stuck_out_tongue: ).

I will keep brainstorming for ideas that might work for my AI - but they used a type of A* in CC, which has a smaller resolution grid of nodes, and does some ā€œmagicā€ to determine paths the ai can reach - and they can reach anywhereā€¦ Definitely check it out if you havenā€™t already!

Edit: I might tear into this code and see what they do -

http://www.openlierox.net/

1 Like

interesting, will have to astudy this. it should be possible since the pixels leave a changed squad zone after falling right?

Hrmm straight c++ isnā€™t my strong point, but I have started figuring out a bit about how these guys handle ai just from observation. Their AI and players can use grappling hooks, and seems to use a A* as far as I can tell, one which doesnā€™t need to consider gravity or anything else because with grappling hooks they can go anywhere basically. Anyway I might try digging in a bit more and see if anything of use is hidden in there haha.

I could be mistaken, and they might be obstacle avoiding dumb ai, who just go towards a player or enemy direction and avoid collisions along the way.

1 Like

@MD_Reptile have you seen this one,
A* Pathfinding to a 2D Grid-Based Platformer

(Combined posts)

Yep mgear I have been seeing that one while googling around, and if I have a breakthrough on the a* speed side of things, Iā€™d definitely try implementing something like that page describes.

(Second post)

Ok, been doing more A* testingā€¦ finally getting better results after a bit of work:

As you can see in that pic, I am getting over 1000 ā€œpixelā€ long paths (meaning closer to 1000 * 8 total nodes checked) in under a second and a half!

Not bad at all! Strangely enough, it seems how I have it set up, it loses that speed at bigger sizes. A 4000 pixel long path took more like 25 seconds, while a 8000 pixel long path took over 2 and half minutes.

So yeah, still plenty of kinks to work out, obviously something ainā€™t right since those ratioā€™s donā€™t match up, but at least the shorter pathfinding is working well!

PS hope you donā€™t mind me going semi-off topic all the time in your thread Joe :stuck_out_tongue:

Iā€™ll keep cracking away at this problem.

1 Like

I donā€™t mind. I have one suggestion: during development & debugging, have your pathfinding algorithm call SetPixel on each pixel it expands as it goes. This may provide some insight into what itā€™s doing on longer paths, and will certainly make some pretty pictures. :slight_smile:

(As it happens, years ago I wrote an article on path-finding in REALbasic, which had a visualization like that. It was really interesting to look at different variations on the algorithm ā€” depth-first aka greedy, breadth-first, and best-first aka A*.)

1 Like

(combined previous post)
I actually just implemented something like that yesterday, but just for the actual path, rather than say, all visited nodes in the search, but I was tinkering with that kind of idea.

Iā€™ll have to rewrite it to color code and create a visual of the whole process, and that way it should look pretty nifty, and actually help me figure how well itā€™s working better!

Hrmm, REALbasic huh, a variant of BASIC Iā€™ll assume? Perhaps I can cheat sheet that a little thenā€¦ been years since I mucked about in BASIC thoughā€¦

Prepare for incoming pictures of the resulting silly mess within the coming few days, or sooner if the coding gods permit.

(new post below)

Sneak preview :stuck_out_tongue: - In this shot, its just a line for the path itself, from player up to the top right corner of the world (no considerations for gravity or jumping, just raw A*)

1 Like

Neat! Though if this case is typical, itā€™s just screaming for a depth-first (i.e. greedy) search rather than A*ā€¦