Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

[RELEASED] PixelSurface: Efficient Destructible 2D Terrain

Discussion in 'Assets and Asset Store' started by JoeStrout, Nov 13, 2015.

  1. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Our PixelSurface asset has just been released in the Asset Store!



    PixelSurface creates a 2D pixel environment. You have complete control over how this surface is divided into tiles for efficient processing, as well as how big the pixels appear on screen. It supports full 32-bit color, with transparency, and standard drawing methods including line, rectangle, ellipse, and flood fill ("paint bucket").

    PixelSurface also supports "live" (dynamic) pixels, which move around on top of the static background. These are perfect for special effects like flying debris, fire, falling snow or rain, and much more.

    Now you can easily create retro-style games that rely on a destructible terrain, in the style of Lemmings, Worms, or Dig Dug, or modern smash hits like Sandbox. Let your imagination go wild — what could you create with this new capability?

    Read the fine manual, or try the web player or WebGL demo! Or go directly to the Asset Store page.

    And if you have any questions or comments, please post them here. I've been an active member of the community for years and if I don't respond quickly, it probably means I am either lost in the desert, or trapped under something heavy. (Please send help.)
     
    Last edited: Jan 13, 2023
    Manny Calavera and theANMATOR2b like this.
  2. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Hey all! I've verified that PixelSurface works perfectly under the latest version of Unity (5.3.0f4), and that it works in WebGL. In fact I've added a WebGL demo, so you can try it yourself!

    Happy pixeling!
    — Joe
     
    frekiidoochmanz likes this.
  3. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Incidentally, I've started a thread in the Design forum brainstorming various ways that PixelSurface could be used to make a unique game. This might be useful to PixelSurface users, or people just looking for some fresh ideas!
     
    theANMATOR2b likes this.
  4. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    Fascinating. I will most likely buy this and tear it apart just to figure out how you got it running so smooth!

    I must know, do you use texture2d's and Apply() to update changes? Maybe a rendertexture method?

    How about dynamic stuff like bombs, or the pixels thrown from an explosion? Are they sprites? Are they drawn to a canvas and updated frame by frame?

    What is the max size of the 2d area? Over the texture size limits on some platforms (like say, over 4096x4096)?
     
    Last edited: Mar 1, 2016
    JoeStrout likes this.
  5. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    It is Texture2D's and Apply, but there are a host of optimizations to make it faster. For example, the surface is divided into subregions, so we only need to Apply in ones that have any changes; and even where there are changes, we batch up all the changes within a frame, and Apply only once.

    As for dynamic stuff, you can either draw Sprites on top of (or behind) your SpriteSurface, or you can draw directly into the SpriteSurface. The demo shows both approaches: the moving pixels (snow, explosion debris, fire) are all "live pixels" which draw directly into the SpriteSurface, whereas the falling bomb is a sprite drawn on top.
     
  6. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    That is almost the same approach I've taken, in "Pixel Destruction", which I spent about 2 years developing (its been shelved for now), and was planning on working on in the future, but perhaps your system could provide me with some optimizations I hadn't figured out yet!

    I'll let you know if I have any problems, thanks for creating the asset!
     
    JoeStrout likes this.
  7. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    Had a problem occur to me that you might have an idea for - how in the world do you handle pathfinding in this scenario? A* seems like the obvious choice given a grid of points to work with, however, using a 4096 x 4096 level as an example, this means 16.7 million points to check against. And being that the level can change, those points need updated for whether or not they are continuing to be "open" for characters to pass through. I instant messaged to data of datarealms a bit about this topic, and how they handled that in Cortex Command. He described it to me as a lower resolution grid, which is nowhere near as detailed as the graphical representation, that's updated using tricks like diagonally testing all the individual pixels for "openness" then assuming the large pathfinding node is open. This avoids updating by the thousands (for instance if you checked against cells of textures, perhaps 128x128 sections) which are open and which aren't, and just checks a diagonal section of a given area, and considering it open "enough".

    What are your thoughts on that? Is there a simple way to do ai pathfinding, more complex that just "run right till you hit obstacle, attempt jumping it, if that fails, run the other way"? And at the same time not so performance hungry as checking 16.7 million pixels for whether or not they are open after modifying the level?

    Edit: Also, Data of CC also mentioned they did a percentage of openness, like they cast lines at different angles in a "node", a pathfinding square area, and if certain percentages were open, it's open enough for ai to pass through...

    Edit2: for the record, trying to test against "cells", the split texture 2d's of the level, that have been updated in a frame for every single pixel in that cell, then taking a percent of that result for whether or not it is open, is too expensive for mobile performance, and frankly won't work well on pc or consoles either.
     
    Last edited: Mar 3, 2016
  8. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Right, I probably wouldn't do A* on a pixel level, though you could, and if the path is reasonably direct, it wouldn't actually check most of those points. But if it had to backtrack, then yeah, that would be painful.

    So yeah, to make it efficient you need to lump open pixels together into a network of regions. A lower-resolution grid is a decent (and certainly the simplest) way to do that. Here are some other variations.

    But people often reach for a search algorithm (typically A*) when a more problem-specific solution could work better. For example, if you have only a couple of destinations you commonly path towards, then you should make an maintain a distance map to those destinations. And in this case you actually could use a PixelSurface for your data structure, which would be great for visualizing it for debugging purposes — just have the amount of (say) green reflect your distance, in steps, to the goal. (Of course a big array of byte or short would be more efficient in this case.)

    This sort of distance map is very easy to update gradually: when anything changes, you simply throw the changed pixel locations onto a "to-do" list, and then on each step, you pull a pixel location off the to-do list, figure its distance is one more than the shortest distance stored for any of its neighbors. If that answer is different from the previous distance you had for that location, then you throw its neighbors onto the to-do list. So you can just crank through N (100 or whatever) to-do items per frame, and the distances will very quickly sort themselves out.

    Now to use this is trivial: from any position on the map, to move towards the destination, you simply move to the neighbor with the shortest distance.

    This can even work when destinations are changing, as long as you can allow your agents to ponder for a few seconds before choosing a path. This might be fine in an RTS, for example. It can actually look quite believable when you occasionally tell your units to go to point X, and they immediately start moving in the direction of X, but then a few seconds later realize that that path is blocked, and change directions to go a different way.
     
    MD_Reptile likes this.
  9. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    Thanks for the thorough answer! I hope I can get it all sorted out before long, especially how to consider 2D gravity and jumping into the pathfinding, that should be just days of fun :X

    You make some good points and I will see what I can figure out. Still haven't made the purchase for your asset, but I should get some free time soon to grab a copy and check it out!
     
    JoeStrout likes this.
  10. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Thanks, and if you do, please write a review! A few people have bought it, but nobody has taken the time to review it yet, and I think some people get nervous about any asset with no reviews.

    Good luck with your project!
     
  11. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    I certainly will, I understand the work that goes into this and honestly think you should be charging more than 20 bucks! At least at surface value :p

    Hope it meets my expectations, but judging from the webgl demo, seems impressive.
     
  12. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Well, if it fails to meet your expectations in any way, just send me a message — I'll be eager to make it right!
     
  13. mgear

    mgear

    Joined:
    Aug 3, 2010
    Posts:
    9,350
    Is it possible to "wakeup" pixels above the explosion, so that they would fall down?
    (as in the old Scorched Earth game, explosion would cause terrain above it to fall down)
     
  14. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Sure! The current code already does something a bit like that: it checks all the pixels in within the explosion radius, and then with some percentage chance, either erases them or converts them into live pixels.

    So, what you could do instead (or in addition) is, check the pixels above this area, and throw any non-empty pixels onto some list. Then on each frame, process the pixels on this list: convert them to live pixels (perhaps with only some probability), and check the pixels above those — throw them on the work list too.

    So in this way, the explosion would cause pixels to convert (bottom-up) to dust, which then falls down. And dividing it over multiple frames like this both makes it more visually interesting, and avoids any sudden drop in the framerate. By fiddling with the percentage chance of being "shaken loose," you could make it so that small bridges or whatever collapse completely, but thick caverns merely drop some roof but then tend to stop. (And to smooth it out, you should probably check not just the pixels directly overhead, but ones to the left and right too, with a lower probability.)

    I know this is a bit vague, but I have to run soon — if it's not enough to get you going, just let me know and I'll try to whip up a demo for you later this weekend!
     
    mgear and MD_Reptile like this.
  15. mgear

    mgear

    Joined:
    Aug 3, 2010
    Posts:
    9,350
    Purchased for testing!

    current idea is here (image below) - if you have time to make demo that would certainly help.. but no rush :)
    landmass_gravity.png

    Other random ideas/feedback after quick test:
    - Having very simple random terrain generation might be nice to have (having randomize method/button to generate levels, either just rolling hills like Scorched Earth, or more complicated like Worms / Liero) *Although it seems to be simple to do, just create any texture and feed to the pixsurf..
    - Water fluid pixels (then could create small lakes on the terrain, and when the side explodes, water pours away, probably works for lava also)
    - DropSand demo script could have option for amount (more fun to pour lots of sand :) )
     
    theANMATOR2b likes this.
  16. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    For that picture above I figure you could just have it take the edges of the explosion, "cast" lines upwards, and flood fill until empty pixels...

    Just an idea I guess, wouldn't know where to start implementation ;p

    I haven't purchased yet, should get a chance this week.

    PS I second the amount idea, would like to stress test it doing dynamic pixels. Perhaps share the largest sized levels you've been able to run on mobile?

    I don't feel like it's a competition lol, but using my own per pixel destruction demo I went up to 4096x4096, or 8192x2048 and so on... could push it further, but performance starts to suffer on older devices (galaxy exhibit) any higher than that.

    PSS oh BTW @mgear I did a test of noise based "rolling hill" 2d level generation, if I can dig it up I'll post it here, no guessing if I got it saved somewhere sensible. If not I'll try and recreate it, as it was kinda nifty for these kinda games.
     
    Last edited: Mar 20, 2016
  17. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Thanks guys, those are good ideas!

    On the gravity thing, I wouldn't do a flood fill all the way up above the explosion (until you hit empty space) and convert them all at once. Instead, just convert the pixels directly above the explosion... and then when they fall, check the pixels above them... and repeat. I'll see if I can make a demo later today.

    I don't think terrain generation is within the scope of the asset, but it would be handy for the demo, so I'll do some of that too and make the code available! (But @mgear, you're right, you can take any old texture — as long as it has "Read/Write Enabled" — and just feed it to PixelSurface via the DrawTexture method.)

    And I think liquids (water, lava) would be a pretty simple behavior for LivePixels... but again I'll put my code where my mouth is, and try to whip something up today.
     
  18. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    OK, here's a little example of random terrain generation. I was originally thinking "caves," but what came out of it is more like "platforms," and it's so cool that I just ran with it. :)



    Here's the code:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Events;
    3. using System.Collections.Generic;
    4. using PixSurf;
    5.  
    6. public class RandomCaves : MonoBehaviour {
    7.     public Vector2 noiseOrigin;
    8.     public Vector2 noiseScale = new Vector2(8, 10);
    9.     public float threshold = 0.5f;
    10.     public Color clearColor = Color.clear;
    11.     public Color rockColor = Color.gray;
    12.     public Color grassColor = Color.green;
    13.    
    14.     void Start() {
    15.         GenerateCaves();
    16.         GrowGrass();
    17.     }
    18.  
    19.     public void GenerateCaves() {
    20.         PixelSurface surf = GetComponent<PixelSurface>();
    21.         surf.Reset();
    22.         Vector2 surfSize = new Vector2(surf.totalWidth, surf.totalHeight);
    23.         for (int y=0; y<surf.totalHeight; y++) {
    24.             for (int x=0; x<surf.totalWidth; x++) {
    25.                 float sampleX = noiseOrigin.x + x * noiseScale.x / surfSize.x;
    26.                 float sampleY = noiseOrigin.y + y * noiseScale.y / surfSize.y;
    27.                 float sample = Mathf.PerlinNoise(sampleX, sampleY);
    28.                 Color c = (sample < threshold ? clearColor : rockColor);
    29.                 surf.SetPixel(x, y, c);
    30.             }
    31.         }
    32.         Debug.Log("Set " + surf.totalWidth +"x" + surf.totalHeight);
    33.     }
    34.    
    35.     public void GrowGrass() {
    36.         PixelSurface surf = GetComponent<PixelSurface>();
    37.         for (int x=0; x<surf.totalWidth; x++) {
    38.             int grassDepth = 0;
    39.             for (int y=surf.totalHeight-1; y>=0; y--) {
    40.                 if (surf.GetPixel(x, y) == clearColor) {
    41.                     grassDepth = Random.Range(1,3) + Random.Range(1,3);
    42.                 } else if (grassDepth > 0) {
    43.                     surf.SetPixel(x, y, grassColor);
    44.                     grassDepth--;
    45.                 }
    46.             }
    47.         }
    48.     }
    49. }
    To try it out for yourself:
    1. Make a new scene.
    2. Create a new empty GameObject, add a PixelSurface component, and configure it as shown in the image above.
    3. Attach the above RandomCaves script.
    4. Run.
    There is of course a lot that could be done to extend this script and make it better, such as picking slightly different shades based on the gradient of the noise function, adding random rocks within the dirt, making the grass stick up a bit on top, etc.

    And of course we're only doing one sample of the noise function here; typically you can get more interesting & detailed results by sampling it multiple times, at different scales, and just adding them together. But I rather liked the look of the smooth platforms & blobs produced by this simple method.
     
    frekiidoochmanz, mgear and MD_Reptile like this.
  19. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    Very cool! Much more in depth than the random hilly thing I was jabbering about, thanks a lot joe!
     
    Last edited: Mar 20, 2016
    JoeStrout likes this.
  20. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    I worked a bit this evening trying to make a LivePixel subclass that acts like a fluid. I don't have it quite right yet, though — it's harder than I had first thought! My fluid particles tend to want to overlap, whereas a real fluid should be incompressible, so the particles never overlap. But making the pixels behave that way is harder than it sounds.

    I'll mull this over for the next few days, and get back to you when I have some good news!
     
  21. mgear

    mgear

    Joined:
    Aug 3, 2010
    Posts:
    9,350
    JoeStrout and MD_Reptile like this.
  22. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Well all live pixels are updated every frame. But yeah, you can also just treat the whole pixel surface as a cellular automaton. The tricky part would be optimizing it, so you don't waste a ton of time checking areas where there isn't any water.

    Thanks for those links, they look really great! It should be pretty trivial to take that code as it is and just convert it to use PixelSurface. I'll definitely study that later when I get a chance.
     
    MD_Reptile likes this.
  23. mgear

    mgear

    Joined:
    Aug 3, 2010
    Posts:
    9,350
    Last edited: Mar 23, 2016
    JoeStrout likes this.
  24. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    @mgear looking good!

    @JoeStrout
    Purchased today, and I like how things are working so far! Very easy to setup and start tinkering.

    First thing I noticed was using a larger surface means a bigger hiccup at the start of the scene in play mode. Using a 2048x2048 image it took a lot longer than the example to get going, so I had a suggestion for it, which is to generate the pixel surface pre-runtime, to shave off whatever performance cost that has. This obviously wouldn't be good for a randomly generated level though, unless of course you want to generate it just once in the editor I guess, so perhaps some kind of helper editor script that can create the pixel surface at edit time, but if you choose not to use it, it just generates it at runtime like it already does.

    Gonna bugger around some more, and I'll report back if I come up with anything else.
     
    JoeStrout likes this.
  25. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Looks great! I can see that you're converting all the pixels at once, though, which I still don't recommend.

    But even if you do prefer it that way (it gives more of a "falling as one big section" feel), you should iterate upwards (in the +Y direction) in each column, but stop as soon as you hit an empty pixel. This will keep the explosion from dislodging things that aren't connected to where the boom occurred.

    If you want to post your modified GoBoom code, I'd be happy to get more specific about what I mean. :) Sometimes it's easier to explain things in code than in words!
     
  26. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Great! I can't wait to see what you make with it.

    That's an interesting idea. My son is using it in a Scramble clone, with two PixelSurfaces laid out end-to-end, each one I think 512 by 4096 in size. I had him do it this way because each one represents a different "section" of the game (if you remember Scramble, it's basically divided into five levels, but they are arranged next to each other and you basically fly directly from one to the next with no break), and they are easier to work on this way. But I also thought that it might be easier for Unity to cull things that aren't in view this way.

    However, there is a cost to doing this: live pixels can't easily transfer from one PixelSurface to another. So, you wouldn't do this in cases where you want that to happen. Instead you would just adjust your PixelSurface chunk size. Chunks are completely transparent to both LivePixels and your own GetPixel/SetPixel code, but they still get culled by Unity. So you're free to adjust chunk size in whatever way makes the best culling vs. draw calls trade-off for your game.

    However, I don't think chunk size will affect that initial launch time you're talking about. I don't see any noticeable hiccup on launch in my son's Scramble game, so I wonder what the difference is? I'll give it some thought!
     
  27. frekiidoochmanz

    frekiidoochmanz

    Joined:
    Feb 7, 2016
    Posts:
    158
    This is going to be a horribly odd question but as my wallet is raised up in my hand I must know:

    COULD you do this with your engine?
    http://store.steampowered.com/app/209670/





    I read alot about the subject,and I've tried to do some experiments of my own, but C++ is difficult for me right now. I believe the idea is managing staticmoving or dynamic pixels VS static and nondynamic. I really wanted to know if your engine could handle maybe large explosive particles, weighted pixels, and even the spawning of dynamic moving chunks of parts(like a falling tile which can also be exploded and broken apart)

    Anyway, I'm gonna buy it anyway was just wondering.... thank you.

    oh sorry, I also saw this article:
    http://gamedevelopment.tutsplus.com...in-how-to-make-everything-explode--gamedev-45

    it shows an interesting raycasting technique on the terrain I think when its not destroyed it casts a ray to detect input or something.
     
  28. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Yes, that looks to me like it's basically using a pixel engine; PixelSurface would be a great way to implement it in Unity.

    Where he's plopping base parts down into the terrain, you would just use PixelSurface.DrawTexture. If you need to do some validation on where you're building — for example, you can't overlap existing parts, or you can't build parts in midair that aren't supported by anything — you would need to just scan the area you're about to draw with GetPixel.

    And then where he's got the digger digging holes around the base, yeah, that's totally just chewing through pixels — that is, erasing within some radius, while converting a fraction of them into LivePixels for visual effect. It's really no different from what the PixelSurface demo does with the "boom" tool. (To make the edges more jagged, you would have to refine this a bit, but it won't be too hard.)

    You asked about a couple more things:
    • Large explosive particles: LivePixels are just single pixels. You could make bigger chunks by either grouping some LivePixels together with your own data structure, or just use a sprite. I'd probably go the sprite approach. You can always plop the sprite back down into the surface (with DrawTexture) and then, if desired, convert it into LivePixels if you want it to shatter.
    • Weighted pixels: if I understand what you mean, I think the demo already shows this — the explosion debris pixels have weight and fall with simple physics.
    • Dynamic moving chunks: I think this is the same thing as the large explosive particles I addressed above, but maybe I misunderstood.
    This should be a very cool project — please keep us posted on your progress, and let me know if you run into any trouble!
     
    frekiidoochmanz likes this.
  29. frekiidoochmanz

    frekiidoochmanz

    Joined:
    Feb 7, 2016
    Posts:
    158
    Thanks alot for the response, I'm definitely going to research it and see what I can do.
     
    JoeStrout likes this.
  30. NicBischoff

    NicBischoff

    Joined:
    Mar 19, 2014
    Posts:
    204
    I grabbed this asset because it looks awesome :) I would love a few more examples, different sized canvas's etc.
    I think there also needs to be an example that shows how gameobjects can respect the pixels in terms of collisions etc. maybe a character that jump around and shoots bullets into the mountain side?
     
    JoeStrout likes this.
  31. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    OK, thanks for the feedback. I'll look into making some more examples.

    But note that the demo does show a sprite interacting with the PixelSurface — when you drop a bomb, the bomb is a sprite that explodes when it touches a non-empty pixel. Here's the key bit of Bomb.cs:

    Code (CSharp):
    1.     void CheckHit() {
    2.         Vector2 pixelPos = surface.PixelPosAtWorldPos(transform.position);
    3.         if (pixelPos.y < -32) {
    4.             // We've fallen off the bottom.
    5.             GameObject.Destroy(gameObject);
    6.             return;
    7.         }
    8.  
    9.         if (!surface.InBounds(pixelPos)) return;
    10.  
    11.         Color c = surface.GetPixel(pixelPos);
    12.         if (c.a > 0.1f) {
    13.             // We've hit something!  Do our explosion effects.
    14.  
    So basically we get our position on the SpriteSurface by calling PixelPosAtWorldPos, and then do some simply bounds tests. Then, we call GetPixel to check the color at that point. In this case I'm just using the alpha of the color to decide whether it is "clear" or not — but of course you could do whatever logic you need there.
     
  32. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    Haha, awesome! That very link to that demo project in java is what I originally based pixel destruction on! That guy is great and went into a lot of detail about doing this sort of thing, and I converted all that java into c# (with the help of people from the community, like mgear I believe assisted in ways) and even got characters using "sprites and bones" going with it, explosion effects, digging, bullet penetration, but haven't finished building mechanics at all.

    Plus - I spoke with Dan Tabar of Data Realms a couple times about cortex command (great game, go buy it) and how they handled pathfinding, and different stuff. Those are great resources for ideas and to learn more about this stuff. If you were particularly interested, you cold join the planetoid pioneers slack, and talk with an army of dedicated cc fans, and try out the test version of data realms next game, and on top of all that, I keep a small section where I bs about pixel destruction there.

    Anyway, yes sir, you are looking at the right places for ideas for this asset :)


    PS - I'm falling in love with this thread. You guys keep the cool stuff rolling in haha
     
    Last edited: Mar 23, 2016
    frekiidoochmanz and JoeStrout like this.
  33. mgear

    mgear

    Joined:
    Aug 3, 2010
    Posts:
    9,350
    Here's current collapse boom script attached, works ok so far.

    Comments:
    - surf.FillCircle(x,y,radius, color) would be nice, since FillEllipse needs Rect, i'd just want to splat simple circle to x,y or vector2
    - If dig hole all to way to bottom (y=0), then livepixels disappear in to the void?


     

    Attached Files:

    Last edited: Mar 23, 2016
    frekiidoochmanz and JoeStrout like this.
  34. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Looks great! If you want to make it look a little more natural, you could, as you're tracing up, sometimes step out to the side a bit. This will give you a more cone-shaped crater above the boom. Of course if you're doing this the way I think you're doing this, you'd have to rewrite that loop pretty much completely. :)

    I can see that. Of course it's simple enough to add yourself:

    Code (CSharp):
    1. void FillCircle(float x, float y, float radius, Color color) {
    2.     surf.FillEllipse(new Rect(x-radius, y-radius, radius*2, radius*2), color);
    3. }
    ...assuming you put this in a context that has a "surf" property. But it's a good idea; I've already added it to the code for the next release (which I expect to put out in the next week or two).

    Yes! That's a feature. But if you don't want that, there are two options: (1) modify the code of your LivePixel subclass (probably PixParticle) to stop when it reaches the edge of the surface; or (2) put a row of pixels at the bottom of your surface that are never destroyed by the explosion.

    I would probably go with option 2. You could just modify the GoBoom code so it ignores the last row of pixels, but it would be more flexible to define some colors as indestructible (representing metal, bedrock, or whatever), and make GoBoom ignore those. And then just make sure your starting texture has a row of that color on bottom.

    If you don't want this bottom row to be visible to the player, simply size the PixelSurface slightly bigger than the screen.
     
  35. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    I was thinking about this today. This is something Data Realms didn't do with Cortex Command, and probably because it is no walk in the park! In the case of using unity, I suppose one could "slice" a chunk of the level off, and make it a dynamic object with gravity easily - but the problem after say, making it a sprite of its own, you need it to collide properly.

    I thought the 2d collider you can create points for would maybe do the job, but when your collisions are based on empty pixels or solid pixels from the level, how do you allow unity's box2d physics to work with that? Short answer in my research - you probably don't!

    What it basically comes down to (in my head anyway haha) is some sort of complex shape collider built to work from the ground up for a "pixel world". It would have to be able to test lines of the shapes (which hopefully could rotate and such) against the points in the world its near, for collisions. This means many many GetPixel calls, or what I did in pixel destruction is kept an array of boolean values to tell me whether or not a pixel is solid, in the interest of faster performance than GetPixel a whole bunch. It is doable but complicated, and would mean higher requirements for android (which I target). Maybe if it were kept down to a lower resolution "mesh" for the 2d collider that generalized the shape of the cutout piece, that could help slightly, but theres no getting around the checks against whats open and whats not, and if the object moves mildly fast, the physics checks would have to run rapidly to prevent objects going through the level.

    Perhaps there would be a better approach to it. I may tinker around with primitive collider shapes, like a cube, and see if I can't make it able to rotate and sit on uneven terrain. Hmmm...

    It is fairly simple making a character controller that stays upright - here is basically what I do (it is messy, testing code!):
    Code (CSharp):
    1.  
    2.     public void checkPhysics()
    3.     {
    4.         // add gravity
    5.         velY -= 980 * Time.fixedDeltaTime;
    6.  
    7.         // Always add x velocity
    8.         x = (x + velX * Time.fixedDeltaTime);
    9.  
    10.         // only add y velocity if not on the ground.
    11.         if (!(onGround && velY < 0))
    12.             y = (y + velY * Time.fixedDeltaTime);
    13.      
    14.         // allow the object to do collision detection and other things
    15.         checkConstraints ();
    16.     }
    17.  
    18.     // checkConstraints - implemented as a PhysicsObj
    19.     public void checkConstraints ()
    20.     {
    21.         if(onGround && ourAnimScript.Jumping)
    22.             ourAnimScript.Jumping = false; // to only stop our jump anim if we are grounded now
    23.  
    24.         if (targetCamera != null)
    25.         {
    26.             if (targetCamera.TrackPlayer != true)
    27.                 targetCamera.TrackPlayer = true;
    28.             targetCamera.TargetPosition = new Vector2(x, y); // tell our camera to follow our movement
    29.         }
    30.         else
    31.             Debug.LogWarning("Warning: targetCamera is NULL!");
    32.      
    33.         MovementCheck();
    34.      
    35.         // Collision detection/handling
    36.         // Loop along each edge of the square until we find a solid pixel
    37.         // if there is one, we find out if there's any adjacent to it (loop perpendicular from that pixel into the box)
    38.         // Once we hit empty space, we move the box to that empty space
    39.      
    40.         GroundCheck();
    41.      
    42.         TopCheck();
    43.      
    44.         EdgeCheck();
    45.      
    46.         BoundaryChecks();
    47.     }
    48.  
    49.     void MovementCheck()
    50.     {
    51.         // movement
    52.         if (moveState == "walkLeft") // controller input is saying were walking left
    53.         {
    54.             if (velX < -moveSpeed)
    55.                 velX = -40;
    56.             else
    57.                 velX -= 2;
    58.         }
    59.         else if(moveState == "runLeft") // were running
    60.         {
    61.             if (velX < -(moveSpeed * 2))
    62.                 velX = -80;
    63.             else
    64.                 velX -= 2;
    65.         }
    66.         else if (moveState == "Idle" && velX < 0)
    67.             velX *= 0.8f; // slow down side-ways velocity if we're not moving left
    68.      
    69.         if (moveState == "walkRight")
    70.         {
    71.             if (velX > moveSpeed)
    72.                 velX = 40;
    73.             else
    74.                 velX += 2;
    75.         }
    76.         else if(moveState == "runRight") // were running
    77.         {
    78.             if (velX > (moveSpeed * 2))
    79.                 velX = 80;
    80.             else
    81.                 velX += 2;
    82.         }
    83.         else if (moveState == "Idle" && velX > 0)
    84.             velX *= 0.8f;
    85.     }
    86.  
    87.     void GroundCheck()
    88.     {
    89.         onGround = false; // onground starts out false and will remain unless set to true
    90.      
    91.         for (int bottomX = (int)x - (playerWidth / 2); bottomX <= (int)x + (playerWidth / 2); bottomX++)
    92.         {
    93.             if (GameMaster.GM.pixelMagic.isPixelSolid (bottomX, (int)y - (playerHeight / 2) - 1) && (velY < 0))
    94.             {
    95.                 if(ourAnimScript.Jumping)
    96.                 {
    97.                     ourAnimScript.Jumping = false;
    98.                 }
    99.                 onGround = true;
    100.              
    101.                 for (int yCheck = (int)y - (playerHeight / 4); yCheck < (int)y - (playerHeight / 2); yCheck++)
    102.                 {
    103.                     if (GameMaster.GM.pixelMagic.isPixelSolid (bottomX, yCheck))
    104.                     {
    105.                         y = yCheck - (playerHeight / 2);
    106.                         break;
    107.                     }
    108.                 }
    109.                 if (velY < 0)
    110.                 {
    111.                     velY *= -0.25f;
    112.                 }
    113.             }
    114.         }
    115.     }
    116.  
    117.     void TopCheck()
    118.     {
    119.         topBlocked = false;
    120.         // start with the top edge
    121.         for (int topX = (int)x - (playerWidth / 2); topX <= (int)x + (playerWidth / 2); topX++)
    122.         {
    123.             if (GameMaster.GM.pixelMagic.isPixelSolid (topX, (int)y + (playerHeight / 2) + 1)) // if the pixel is solid
    124.             {
    125.                 topBlocked = true;
    126.                 if (velY > 0)
    127.                 {
    128.                     velY *= -0.5f;
    129.                 }
    130.             }
    131.         }
    132.     }
    133.  
    134.     void EdgeCheck()
    135.     {
    136.         rightBlocked = false;
    137.         leftBlocked = false;
    138.      
    139.         // loop left edge of our players collision box
    140.         if (velX < 0) // since velocity is negative were moving left.
    141.         {
    142.             // set our anim to go left
    143.             if(ourAnimScript.Mirror != true)
    144.                 ourAnimScript.ChangeMirror(true);
    145.          
    146.             for (int leftY = (int)y - (playerHeight / 2); leftY <= (int)y + (playerHeight / 2); leftY++) // start at bottom left of player, iterate upwards
    147.             {
    148.                 if (GameMaster.GM.pixelMagic.isPixelSolid ((int)x - (playerWidth / 2), leftY)) // if our x pixel to our left side at the iteration of leftY height is solid...
    149.                 {
    150.                     // next move from a fourth of our width inside from left edge...
    151.                     for (int xCheck = (int)x - (playerWidth / 4); xCheck < (int)x - (playerWidth / 2); xCheck--) // and then iterate across towards our left side
    152.                     {
    153.                         if (GameMaster.GM.pixelMagic.isPixelSolid (xCheck, leftY)) // if the check position is solid, were colliding
    154.                         {
    155.                             leftBlocked = true;
    156.                             x = xCheck + (playerWidth / 2); // push the player over and break out of the loop
    157.                             break;
    158.                         }
    159.                     }
    160.                     if (leftY < y && !topBlocked) // if we havent broken out of the loop with a collision, and our leftY is lower then our y position, and were not blocked
    161.                     {
    162.                         y += 1; // add one to our y position since a pixel was detected at our feet in this case
    163.                     }
    164.                     else
    165.                     {
    166.                         velX *= -0.4f; // reflect our velocity on x,
    167.                         x += 2; // and give it a bump if were barely moving...
    168.                     }
    169.                 }
    170.             }
    171.         }
    172.         // do the same for the right edge
    173.         if (velX > 0)
    174.         {
    175.             // set our anim to go right
    176.             if(ourAnimScript.Mirror != false)
    177.                 ourAnimScript.ChangeMirror(false);
    178.             for (int rightY = (int)y - (playerHeight / 2); rightY <= (int)y + (playerHeight / 2); rightY++)
    179.             {
    180.                 if (GameMaster.GM.pixelMagic.isPixelSolid ((int)x + (playerWidth / 2), rightY))
    181.                 {
    182.                     for (int xCheck = (int)x + (playerWidth / 4); xCheck < (int)x + (playerWidth / 2) + 1; xCheck++)
    183.                     {
    184.                         if (GameMaster.GM.pixelMagic.isPixelSolid (xCheck, rightY))
    185.                         {
    186.                             rightBlocked = true;
    187.                             x = xCheck - (playerWidth / 2);
    188.                             break;
    189.                         }
    190.                     }
    191.                     if (rightY < y && !topBlocked)
    192.                     {
    193.                         y += 1;
    194.                     }
    195.                     else
    196.                     {
    197.                         velX *= -0.4f;
    198.                         x -= 2;
    199.                     }
    200.                 }
    201.             }
    202.         }
    203.     }
    204.  
    205.     void BoundaryChecks()
    206.     {
    207.         // Boundary Checks
    208.         if (x - (playerWidth / 2) < 0)// && velX < 0)
    209.         {
    210.             Debug.Log("boundary check bumping player X, its under 0! --- x = " + x);
    211.             leftBlocked = true;
    212.             x = 0 + (playerWidth / 2); // move us out of boundary if were going in
    213.             velX = 1; // bump player away from left boundary
    214.         }
    215.         if (y - (playerHeight / 2) < 0)// && velY < 0)
    216.         {
    217.             Debug.Log("boundary check bumping player Y, its under 0! --- y = " + y);
    218.             y = 0 + (playerHeight / 2); // move us out of boundary if were going in
    219.             velY = 1; // bump player off bottom boundary
    220.         }
    221.         if (x + (playerWidth / 2) > worldWidth)// && velX > 0)
    222.         {
    223.             Debug.Log("boundary check bumping player X, its over max worldwidth! --- x = " + x);
    224.             rightBlocked = true;
    225.             x = worldWidth - (playerWidth / 2); // move us into boundary if were going out
    226.             velX = -1; // bump player off right boundary
    227.         }
    228.         if (y + (playerHeight / 2) > worldHeight)// && velY > 0)
    229.         {
    230.             Debug.Log("boundary check bumping player Y, its over max worldheight! --- y = " + y);
    231.             y = worldHeight - (playerHeight / 2); // move us into boundary if were going out
    232.             velY = -1; // bump us away from top boundary
    233.         }
    234.     }
    235.  
    This borrows a bunch from the java project posted above, and the method "checkPhysics" is called on every physics object at an interval of your fixed update (but managed in a physics script). The "isPixelSolid" method is what tells us if a pixel is empty or not. Most of the rest I guess is self explanatory. It does sort of a "box check" of the characters surrounding pixels (ones which should be at the players feet, sides, and above his head) and if they are clear, fall with gravity, unless your grounded on solid pixels.

    But how about making something similar, but a fully dynamic physics object???? It ain't gonna leave my brain till I start testing haha...

    EDIT: Unrelated note - Here is a little pic I got of me running that terrain generator from joe on my own project:


    Works great! Awesome share!
     
    Last edited: Mar 25, 2016
    JoeStrout likes this.
  36. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    PixelSurface has just been reviewed at AppGoodies.net.

    The author seems pretty happy with it overall, but complains:

    I'm having a bit of trouble seeing how this makes any sense. PixelSurface isn't about "pixelating" a sprite; it's about giving you an entire surface of pixels to play in. Sprites really have nothing to do with it, except that (as shown in the demo) you can of course draw sprites on top of (or behind, if you wish) your PixelSurface, and make them interact however you want... but doing so doesn't require doing anything special to the sprite.

    Can anyone make out what the author is getting at here? What would this script he imagines you dragging onto a sprite actually do?
     
    frekiidoochmanz likes this.
  37. mgear

    mgear

    Joined:
    Aug 3, 2010
    Posts:
    9,350
    Sounds like he would like the script automatically set the texture importer [x] read/write enabled, instead of getting that error..

    And then that helper script would then be able to convert any sprite to pixel surface pixels or as pixel surface level(?),
    since it would enable read/write for that texture..

    I don't really see much problems there..user could multi-select images and set them [x] read/write enabled..


    *here is one snippet for setting isReadable for texture in editor, if anyone needs it:
    https://gist.github.com/unitycoder/87dd17a123b008a251b9#file-spritebackgroundremove-cs-L141-L156
     
  38. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    OH! I think I see. He's talking about setting the initial SpriteSurface content from an image!

    So I guess the idea would be:
    1. Import your texture as a "Sprite (2D and UI)".
    2. Drag it to your scene, which creates a GameObject with a SpriteRenderer set to that sprite.
    3. Attach the PixelSurface script, which... um... I guess sets isReadable on the source texture, removes the SpriteRenderer component (which wouldn't make any sense anymore), and configures itself as a surface in the same position and scale as the sprite, with the sprite texture drawn into the surface on Awake.
    Or maybe it leaves the SpriteRenderer component on, so you can continue to see your initial content in the editor. The alternative would be to make PixelSurface render with its initial content at edit time. I can certainly see how either of these would be very helpful when it comes to positioning other things around the SpriteSurface while laying out your level or whatever.

    However, I don't like the "it's a sprite — no, it's a PixelSurface!" nature of this workflow. It sounds like the author was already confused on the relationship between PixelSurface and sprites; this would make it even more confusing. And I see no point in converting a texture into a Sprite, only to convert it back into a texture so we can draw it into the surface.

    Better might be to just have a Texture2D property on the PixelSurface called "Initial Texture" or something like that. When set in the editor, it sets the isReadable flag for you (thanks @mgear for that snippet!), and then configures itself to draw that texture into itself, both in the editor and upon Awake.

    Of course if the user is thinking of it backwards ("I have this Sprite... how do I 'pixelize' it?"), they're still going to be disappointed. The intended approach is the reverse ("I have a PixelSurface... how do I want to initialize it?"). Perhaps that could be helped by including a prefab, already set up with the test pattern.

    Opinions, anyone?
     
  39. frekiidoochmanz

    frekiidoochmanz

    Joined:
    Feb 7, 2016
    Posts:
    158
    The PixelSurface is an object which takes in a texture and works with a surface, which takes in the surfaces pixel data using built in unity API, is that correct?
     
  40. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    No, I wouldn't say that. The PixelSurface is essentially a 2D array of pixels. They are managed into chunks, each of which has a texture internally so Unity can shove it to the video card, but these textures are not exposed to the user.

    Now, for initializing a PixelSurface, drawing a texture is one thing you can do. You can also clear it to a solid color, or initialize it procedurally, as MD_Reptile demonstrated a few posts above this one. So you certainly don't have to have any texture at all to use a PixelSurface; that's just one way of setting the initial content.
     
  41. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    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.
     
    JoeStrout likes this.
  42. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    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!
     
    mgear likes this.
  43. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    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:

    Code (CSharp):
    1. /* Simple water/fluid Cellular Automaton (CA)
    2. based on: http://w-shadow.com/blog/2009/09/01/simple-fluid-simulation/
    3. */
    4.  
    5. using UnityEngine;
    6. using UnityEngine.Events;
    7. using System.Collections.Generic;
    8. using PixSurf;
    9.  
    10. [RequireComponent(typeof(PixelSurface))]
    11. public class FluidCA : MonoBehaviour {
    12.     #region Public Properties
    13.     public bool running;
    14.  
    15.     #endregion
    16.     //--------------------------------------------------------------------------------
    17.     #region Private Properties
    18.    
    19.     // The mass of water at each cell in the grid.
    20.     float[,] mass;
    21.    
    22.     // "new" mass, used only during simulation step (but kept around for efficiency)
    23.     float[,] newMass;
    24.    
    25.     PixelSurface surf;
    26.     int width, height;
    27.    
    28.     const float kMaxMass = 1.0f;        // The normal, unpressurized mass of fluid in a cell
    29.     const float kMaxCompress = 0.05f;    // how much excess fluid a cell can have with just ONE full cell above
    30.     const float kMinMass = 0.0001f;        // at what mass of fluid we consider a cell to be dry
    31.     const float kMaxFlow = 1f;            // max units of water to move out of one block to another per timestep
    32.     const float kMinFlow = 0.01f;
    33.    
    34.     #endregion
    35.     //--------------------------------------------------------------------------------
    36.     #region MonoBehaviour Events
    37.     void Start() {
    38.         surf = GetComponent<PixelSurface>();
    39.         width = surf.totalWidth;
    40.         height = surf.totalHeight;
    41.         mass = new float[width, height];
    42.         newMass = new float[width, height];
    43.        
    44.         Reset();
    45.     }
    46.    
    47.     void Update() {
    48.         // Update the water simulation.
    49.         if (running) DoOneStep();
    50.        
    51.         // For testing: add water upon mouse button 0; with mouse button 1, draw/erase ground.
    52.         if (Input.GetMouseButton(0)) {
    53.             Vector2 pos;
    54.             if (surf.PixelPosAtScreenPos(Input.mousePosition, out pos)) {
    55.                 AddFluid((int)(pos.x), (int)(pos.y), kMaxMass);
    56.             }
    57.         }
    58.  
    59.         if (Input.GetMouseButton(1)) {
    60.             Vector2 pos;
    61.             if (surf.PixelPosAtScreenPos(Input.mousePosition, out pos)) {
    62.                 bool erase = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
    63.                 surf.SetPixel((int)pos.x, (int)pos.y, erase ? Color.black : Color.yellow);
    64.                 ClearFluid((int)pos.x, (int)pos.y);
    65.             }
    66.         }
    67.     }
    68.  
    69.     #endregion
    70.     //--------------------------------------------------------------------------------
    71.     #region Public Methods
    72.    
    73.     public void DoOneStep() {
    74.         // Loop over the grid, updating newMass (which should already be a copy of mass)
    75.         // with new values based on fluid flowing out of each cell.
    76.         for (int y=0; y<height; y++) {
    77.             for (int x=0; x<width; x++) {
    78.                
    79.                 float remainingMass = mass[x, y];
    80.                 if (remainingMass <= 0) continue;    // ToDo: kMinMass?
    81.  
    82.                 // Flow down into the block below.
    83.                 if (Available(x, y-1)) {
    84.                     float massBelow = mass[x, y-1];
    85.                     float flow = GetStableStateBottom(remainingMass + massBelow) - massBelow;
    86.                     if (flow > kMinFlow) flow *= 0.5f;    // loads to smoother flow?
    87.                     flow = Mathf.Clamp(flow, 0, Mathf.Min(kMaxFlow, remainingMass));
    88.                     remainingMass -= flow;
    89.                     newMass[x, y] -= flow;
    90.                     newMass[x, y-1] += flow;
    91.                 }
    92.                
    93.                 // Flow left.
    94.                 if (Available(x-1, y)) {
    95.                     float flow = (mass[x, y] - mass[x-1, y]) / 4;
    96.                     if (flow > kMinFlow) flow *= 0.5f;
    97.                     flow = Mathf.Clamp(flow, 0, remainingMass);
    98.                    
    99.                     remainingMass -= flow;
    100.                     newMass[x, y] -= flow;
    101.                     newMass[x-1, y] += flow;
    102.                 }
    103.                
    104.                 // Flow right.
    105.                 if (Available(x+1, y)) {
    106.                     float flow = (mass[x, y] - mass[x+1, y]) / 4;
    107.                     if (flow > kMinFlow) flow *= 0.5f;
    108.                     flow = Mathf.Clamp(flow, 0, remainingMass);
    109.                    
    110.                     remainingMass -= flow;
    111.                     newMass[x, y] -= flow;
    112.                     newMass[x+1, y] += flow;
    113.                 }
    114.                
    115.                 // Flow up (if compressed).
    116.                 if (Available(x, y+1)) {
    117.                     float flow = mass[x, y] - GetStableStateBottom(mass[x, y] + mass[x, y+1]);
    118.                     if (flow > kMinFlow) flow *= 0.5f;
    119.                     flow = Mathf.Clamp(flow, 0, Mathf.Min(kMaxFlow, remainingMass));
    120.                    
    121.                     remainingMass -= flow;
    122.                     newMass[x, y] -= flow;
    123.                     newMass[x, y+1] += flow;
    124.                 }
    125.                
    126.                 if (remainingMass < 0) {
    127.                     Debug.LogError("Whoops!");
    128.                 }
    129.                 //                newMass[x, y] = remainingMass;
    130.             }
    131.         }
    132.        
    133.         // Copy newMass back into mass, now that all flows are accumulated,
    134.         // and update the display at the same time.
    135.         for (int y=0; y<height; y++) {
    136.             for (int x=0; x<width; x++) {
    137.                 float m = newMass[x, y];
    138.                 if (mass[x, y] != m) {
    139.                     mass[x, y] = m;
    140.                     if (m < kMinMass) surf.SetPixel(x, y, Color.black);
    141.                     else {
    142.                         float a = m/(kMaxMass*2);
    143.                         surf.SetPixel(x, y, new Color(a, a, 1));
    144.                     }
    145.                 }
    146.             }
    147.         }
    148.     }
    149.    
    150.     public void AddFluid(int x, int y, float amount) {
    151.         mass[x, y] += amount;
    152.         newMass[x, y] += amount;
    153.     }
    154.    
    155.     public void ClearFluid(int x, int y) {
    156.         mass[x, y] = newMass[x, y] = 0;
    157.     }
    158.    
    159.     public void Reset() {
    160.         for (int y=0; y<height; y++) {
    161.             for (int x=0; x<width; x++) {
    162.                 mass[x, y] = newMass[x, y] = 0;
    163.             }
    164.         }
    165.         surf.Clear(Color.black);
    166.     }
    167.    
    168.     #endregion
    169.     //--------------------------------------------------------------------------------
    170.     #region Private Methods
    171.    
    172. /// <summary>
    173.     /// Figure out whether the given pixel position is available for fluid to use.
    174.     /// </summary>
    175.     bool Available(int x, int y) {
    176.         if (x < 0 || x >= width || y < 0 || y >= height) return false;
    177.         Color c = surf.GetPixel(x, y);
    178.         // Transparent or pixels are always OK.
    179.         if (c == Color.black || c.a < 0.5f) return true;
    180.         // As are pixels that look like water (i.e., have full blue).
    181.         if (c.b > 0.99f) return true;
    182.         // Otherwise, assume it's ground.
    183.         return false;
    184.     }
    185.    
    186.     /// <summary>
    187.     /// Given a total amount of water, calculate how it should be split between
    188.     /// two cells stacked vertically.  Return the amount of water that should be
    189.     /// in the bottom cell in the stable state.
    190.     /// </summary>
    191.     /// <param name="totalMass"></param>
    192.     /// <returns></returns>
    193.     float GetStableStateBottom(float totalMass) {
    194.         if (totalMass <= kMaxMass) {
    195.             return kMaxMass;
    196.         }
    197.         if (totalMass < 2*kMaxMass + kMaxCompress) {
    198.             return (kMaxMass*kMaxMass + totalMass*kMaxCompress) / (kMaxMass + kMaxCompress);
    199.         }
    200.         return (totalMass + kMaxCompress)/2;
    201.     }
    202.    
    203.     #endregion
    204. }
    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!
     
    frekiidoochmanz, mgear and MD_Reptile like this.
  44. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    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.
     
    JoeStrout likes this.
  45. frekiidoochmanz

    frekiidoochmanz

    Joined:
    Feb 7, 2016
    Posts:
    158
    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))?
     
  46. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    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
     
    Last edited: Apr 6, 2016
  47. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    @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
     
  48. frekiidoochmanz

    frekiidoochmanz

    Joined:
    Feb 7, 2016
    Posts:
    158
    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
     
  49. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    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 :D

    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.
     
    Last edited: Apr 6, 2016
  50. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    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.
     
    MD_Reptile likes this.