Search Unity

[RELEASED] PixelSurface: Efficient Destructible 2D Terrain

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

  1. Euro

    Euro

    Joined:
    Apr 23, 2014
    Posts:
    2
    Would this be a good asset for an Arcanists clone? It looks like exactly what I need as far as terrain destruction goes, but I'd still need to figure out physics and stuff. I have a CS/Software Engineering background but physics definitely isn't my strong suit lol. Arcanists is essentially a worms clone from back in the day that I used to love playing.
     
  2. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    Wow... ok well so I guess I wonder why did you even use the normal char controller? Are you generating a collision mesh for the world? I mean you still must be doing per-pixel checks of whether or not the character has collided with the ground right? And if you are still measuring some kind of box around the character and checking if its hitting solid pixels - why even use the character controller? For locomotion? And if you are generating a collision mesh - isn't that too intense for when the player and AI may be doing lots of changes to the world per frame?

    I'm curious because I always avoided character controllers when doing these types of projects because the "world" was so different than a bunch of sprites with colliders or 3d meshes with colliders... so it didn't make sense to me to even try and use those.
     
  3. Dark_Seth

    Dark_Seth

    Joined:
    May 28, 2014
    Posts:
    140
    Guys any tips on how to group pixels together to make example an ball? Or a plank? That can move together.
     
  4. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    You would probably have the group of pixels as a sprite, and do some physics checking (bounding box or similar) that checks against the open or solid pixels, and add gravity velocity and such yourself through scripting.

    If you want to be able to remove/add pixels to your ball - you may be able to try and experiment with having a small pixel surface represent the ball, and add/remove pixels from that surface somehow - but that might get pretty complicated.
     
  5. Dark_Seth

    Dark_Seth

    Joined:
    May 28, 2014
    Posts:
    140
    Thanks you game me an idead. Will look into that.
     
  6. Dark_Seth

    Dark_Seth

    Joined:
    May 28, 2014
    Posts:
    140
    So if a bomb explodes it should also check if it hits the surface texture of the world and the ball/object.

    An idea but have no idea how to do that is. ( half way there ) I add a sprite to the scene. This sprite is turned into a texture. Add my Colliders etc that I need to interact with the player and the world mesh. I just need a way for the texture to be drawn on the pix surface instead of its own texture. This should also update as the collider rolls about.

    Damn. This seems to be a bit difficult.
     
  7. Christian-Tucker

    Christian-Tucker

    Joined:
    Aug 18, 2013
    Posts:
    376
    @Dark_Seth Would you mind sharing the changes you've made to PixelSurface in order to allow standard character controllers work? I've looked into regenerating the mesh colliders, but they're completely ignored by anything else in the world.

    The next idea I have is to generate edge colliders for each tile, granted there's no LivePixels.

    I would gladly pay the same amount of money as I did for the asset (maybe even more), for a functional character controller.

    I've been working on implementing the first example you gave, and while I have movement and jumping working, walking up small slopes is my next problem.


    EDIT: Generating edges (non-optimal approach) for a polygon collider. Looking to convert them to and EdgeCollider.



    EDIT 2: Got the Edge Colliders working, now just to fix the code for tracing... I've never done anything like this before, so I can't really complain about the weird connections.



    EDIT 3: Any help would be appreciated with this as the more I try to fix it, the more damage I do lol.
     
    Last edited: Dec 2, 2019
  8. Christian-Tucker

    Christian-Tucker

    Joined:
    Aug 18, 2013
    Posts:
    376
    So, I stopped working on the collider implementation as I'm running this on the server and need multiple instances to be active at once, and my benchmarks showed that pixel-scanning was far more performance favored.

    I'm currently struggling with getting smooth movement working for my player, this is what it looks like.



    As you can see it's very, very bouncy.

    Here's the controller code.


    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using PixSurf;
    5.  
    6. [RequireComponent(typeof(BoxCollider2D))]
    7. public class PlayerController : MonoBehaviour
    8. {
    9.     private PixelSurface surface;
    10.     private BoxCollider2D collider;
    11.  
    12.     private Vector2 bottomLeft = Vector2.zero;
    13.     private Vector2 bottomRight = Vector2.zero;
    14.     private Vector2 right = Vector2.zero;
    15.     private Vector2 left = Vector2.zero;
    16.     private Vector2 topLeft = Vector2.zero;
    17.     private Vector2 topRight = Vector2.zero;
    18.     private Vector2 top = Vector2.zero;
    19.     private Vector2 bottom = Vector2.zero;
    20.  
    21.     private bool grounded = false;
    22.     private bool blockedLeft = false;
    23.     private bool blockedRight = false;
    24.     private bool blockedTop = false;
    25.  
    26.     private Vector2 velocity = Vector2.zero;
    27.     private float movementSpeed = 10f;
    28.     private float movementSlope = 12f;
    29.  
    30.     private bool activeEntity = true;
    31.  
    32.     private Dictionary<string, bool> inputs = new Dictionary<string, bool>
    33.     {
    34.         { "left", false },
    35.         { "right", false },
    36.         { "jump", false }
    37.     };
    38.  
    39.  
    40.     private void Start()
    41.     {
    42.         surface = PixelSurface.singleton;
    43.         collider = GetComponent<BoxCollider2D>();
    44.     }
    45.  
    46.     private void Update()
    47.     {
    48.         velocity = Vector2.zero;
    49.  
    50.         if (activeEntity)
    51.         {
    52.             ResetFlags();
    53.             SetRaycastPositions();
    54.             ProcessRays();
    55.          
    56.             if (inputs["right"] && blockedRight == false)
    57.             {
    58.                 velocity.x = movementSpeed / 10;
    59.             }
    60.  
    61.             if (inputs["left"] && blockedLeft == false)
    62.             {
    63.                 velocity.x = -(movementSpeed / 10);
    64.             }
    65.  
    66.         }
    67.  
    68.         if(grounded == false)
    69.         {
    70.             velocity.y = -5f;
    71.         }
    72.         else
    73.         {
    74.             int diff = GetStandingPosition();
    75.             velocity.y = diff;
    76.         }
    77.  
    78.         // if(velocity != Vector2.zero) transform.Translate(velocity);
    79.  
    80.         if (velocity != Vector2.zero)
    81.         {
    82.             Vector2 targetPosition = (Vector2)transform.position + velocity;
    83.             transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * 50);
    84.         }
    85.     }
    86.  
    87.     private void ResetFlags()
    88.     {
    89.         grounded = false;
    90.         blockedLeft = false;
    91.         blockedRight = false;
    92.         blockedTop = false;
    93.     }
    94.  
    95.     private void SetRaycastPositions()
    96.     {
    97.         bottomLeft = new Vector2(collider.bounds.min.x, collider.bounds.min.y + movementSlope);
    98.         bottomRight = new Vector2(collider.bounds.max.x, collider.bounds.min.y + movementSlope);
    99.  
    100.         topLeft = new Vector2(collider.bounds.min.x, collider.bounds.max.y);
    101.         topRight = new Vector2(collider.bounds.max.x, collider.bounds.max.y);
    102.  
    103.         left = new Vector2(collider.bounds.min.x, collider.bounds.min.y + ((collider.bounds.max.y - collider.bounds.min.y) / 2));
    104.         right = new Vector2(collider.bounds.max.x, collider.bounds.min.y + ((collider.bounds.max.y - collider.bounds.min.y) / 2));
    105.  
    106.         top = new Vector2(collider.bounds.min.x + ((collider.bounds.max.x - collider.bounds.min.x) / 2), collider.bounds.max.y);
    107.         bottom = new Vector2(collider.bounds.min.x + ((collider.bounds.max.x - collider.bounds.min.x) / 2), collider.bounds.min.y);
    108.     }
    109.  
    110.     private void ProcessRays()
    111.     {
    112.         float speed = movementSpeed + 1;
    113.  
    114.         if (IsBlocked(topRight, true, true, speed)) blockedRight = true;
    115.         if (blockedRight == false && IsBlocked(right, true, true, speed)) blockedRight = true;
    116.         if (blockedRight == false && IsBlocked(bottomRight, true, true, speed / 3, true)) blockedRight = true;
    117.  
    118.         if (IsBlocked(topLeft, true, false, speed)) blockedLeft = true;
    119.         if (blockedLeft == false && IsBlocked(left, true, false, speed)) blockedLeft = true;
    120.         if (blockedLeft == false && IsBlocked(bottomLeft, true, false, speed / 3)) blockedLeft = true;
    121.  
    122.         if (IsBlocked(bottom, false, false, 3f)) grounded = true;
    123.         if (grounded == false && IsBlocked(bottomLeft, false, false, 3f)) grounded = true;
    124.         if (grounded == false && IsBlocked(bottomRight, false, false, 3f)) grounded = true;
    125.  
    126.         if(grounded)
    127.         {
    128.             if (IsBlocked(top, false, true, 5f)) blockedTop = true;
    129.             if (blockedTop == false && IsBlocked(topLeft, false, true, 5f)) blockedTop = true;
    130.             if (blockedTop == false && IsBlocked(topRight, false, true, 5f)) blockedTop = true;
    131.         }
    132.     }
    133.  
    134.     private int GetStandingPosition()
    135.     {
    136.         Vector2 position = transform.position;
    137.         bool opaque = false;
    138.         int count = 0;
    139.         while(opaque == false)
    140.         {
    141.             if (surface.InBounds(position) == false) return 0;
    142.             if (surface.GetPixel(position).a < 0.1)
    143.             {
    144.                 opaque = true;
    145.                 return count;
    146.             }
    147.             else
    148.             {
    149.                 count++;
    150.                 position.y = position.y + 1;
    151.             }
    152.         }
    153.  
    154.         return count; ;
    155.     }
    156.  
    157.     private bool IsBlocked(Vector2 start, bool horizontal, bool increment, float distance, bool debug = false)
    158.     {
    159.  
    160.         if (horizontal)
    161.         {
    162.             int x = (int)start.x;
    163.             int final = increment ? (int)start.x + (int)distance : (int)start.x - (int)distance;
    164.             while(x != final)
    165.             {
    166.                 if (surface.GetPixel(x, (int)start.y).a >= 0.1)
    167.                 {
    168.                     Debug.DrawRay(start, new Vector2(increment ? distance : -distance, 0f), Color.red, 0.1f);
    169.                     return true;
    170.                 }
    171.  
    172.                 if (increment) x++;
    173.                 else x--;
    174.             }
    175.        
    176.         }
    177.         else
    178.         {
    179.             int y = (int)start.y;
    180.             int final = increment ? (int)start.y + (int)distance : (int)start.y - (int)distance;
    181.             while(y != final)
    182.             {
    183.                 if (surface.GetPixel((int)start.x, y).a >= 0.1)
    184.                 {
    185.                     Debug.DrawRay(start, new Vector2(0f, distance), Color.red, 0.1f);
    186.                     return true;
    187.                 }
    188.  
    189.                 if (increment) y++;
    190.                 else y--;
    191.             }
    192.         }
    193.  
    194.         Debug.DrawRay(start, new Vector2(horizontal ? increment ? distance : -distance : 0f, horizontal ? 0f : increment ? distance : -distance), Color.red, 0.1f);
    195.         return false;
    196.     }
    197.  
    198.     public void SetInput(string key, bool value)
    199.     {
    200.         inputs[key] = value;
    201.     }
    202.  
    203.     public bool IsGrounded()
    204.     {
    205.         return grounded;
    206.     }
    207.  
    208.     public bool IsBlocked(string direction)
    209.     {
    210.         switch(direction)
    211.         {
    212.             case "left": return blockedLeft;
    213.             case "right": return blockedRight;
    214.             case "top": return blockedTop;
    215.             default: return false;
    216.         }
    217.     }
    218. }
    219.  
    220.  
    I've tried with both using `Translate` and `Lerp`. Perhaps you have an opinion on the matter, @JoeStrout



    EDIT: It seems like the issue occurs when moving off of an edge, and the jank is caused by the gravity pulling the unit down.
     
    Last edited: Dec 3, 2019
  9. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    I haven't studied your code, but I bet this is exactly it. To keep things smooth, when grounded you need to find the ground position below your character, and if that's _close enough_, don't apply gravity — just snap to the ground on every frame. Only if the ground is more than some distance away do you switch to falling mode and allow gravity to do its thing.

    HTH,
    - Joe
     
  10. Christian-Tucker

    Christian-Tucker

    Joined:
    Aug 18, 2013
    Posts:
    376
    That was it, exactly. It's still a little rough, but I believe that's because of my 1 unit / pixel ratio. I've never messed with pixels before, and this is the implementation that I came up with before I saw your comment, and it seems pretty similar.


    Code (CSharp):
    1.  
    2. Vector2 targetPosition = (Vector2)transform.position + velocity;
    3. if (velocity.y == 0)
    4. {
    5.     int xDif = (int)(transform.position.x - targetPosition.x);
    6.     Vector2 testPosition = new Vector2(targetPosition.x + xDif, targetPosition.y);
    7.     targetPosition.y += -GetPixelCountToLand(testPosition);
    8. }
    9.  
    10. transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * 50);
    11.  
    Basically, it calculates the target position, and then checks for the ground based on the target position. The `GetPixelCountToLand` function returns the amount of pixels (units) to move the player down, when he moves left/right.

    This is how I'm handling that:

    Code (CSharp):
    1. private int GetPixelCountToLand(Vector2 position)
    2. {
    3.     bool opaque = false;
    4.     int count = 0;
    5.     while (opaque == false)
    6.     {
    7.         if (count >= 5) return 5;
    8.         if (surface.InBounds(position) == false) return 0;
    9.         if (surface.GetPixel(position).a >= 0.1)
    10.         {
    11.             opaque = true;
    12.             return count;
    13.         }
    14.         else
    15.         {
    16.             count++;
    17.             position.y = position.y - 1;
    18.         }
    19.     }
    20.  
    21.     return count;
    22. }

    I'm not sure if this is at all optimal, but it's the best looking solution that I could come up with. The reason I return at a maximum of 5, is because that's my gravity, if the player is too far above ground, he just falls at the standard rate.

    I should add that this is still not smooth, and is likely because I'm messing something up.
     
    Last edited: Dec 4, 2019
    JoeStrout likes this.
  11. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    You're on the right track. That GetPixelCountToLand function could be tightened up a bit (you don't need the opaque variable at all, for example), but it's not the problem.

    Your calculation of testPosition looks very strange to me. Rather than checking the ground where you're about to go (targetPosition), you're checking it at basically your current position (except for some rounding to int). Why not just check targetPosition? (This is likely the problem.)

    The other thing I don't see is how you are handling going uphill. Rather than just checking from your target Y position down, you probably need to also check upwards, and move up until you are on the ground again. Maybe that's in other code you don't show above, but that is another place where it could cause jitter, if the way that works is not in sync with the way your moving-down-to-the-ground code works.
     
  12. Christian-Tucker

    Christian-Tucker

    Joined:
    Aug 18, 2013
    Posts:
    376
    EDIT: The reason I cast to int is because I'm using a 1unit per pixel ratio.

    Still struggling with this, I went through and commented the code so hopefully you could help out. All of my movement related code can be found here:

    Code (CSharp):
    1. private void Update()
    2. {
    3.     velocity = Vector2.zero;
    4.  
    5.     if (activeEntity)
    6.     {
    7.         ResetFlags();
    8.         SetRaycastPositions();
    9.         ProcessRays();
    10.        
    11.         if (inputs["right"] && blockedRight == false)
    12.         {
    13.             velocity.x = movementSpeed / 10;
    14.         }
    15.  
    16.         if (inputs["left"] && blockedLeft == false)
    17.         {
    18.             velocity.x = -(movementSpeed / 10);
    19.         }
    20.  
    21.     }
    22.  
    23.     if(grounded == false)
    24.     {
    25.         velocity.y = -GetPixelCountToLand(bottom);
    26.     }
    27.    
    28.     if (velocity != Vector2.zero)
    29.     {
    30.         // Get the position that we're trying to move to.
    31.         Vector2 targetPosition = (Vector2)transform.position + velocity;
    32.         if (velocity.y == 0 && velocity.x != 0)
    33.         {
    34.             // Get the difference between our position and the target position.
    35.             int xDif = (int)(transform.position.x - targetPosition.x);
    36.  
    37.             // Generate the test position.
    38.             // NOTE: bottom.y is the bottom position of the Player, and is what connects
    39.             //       the player to the ground, as the sprite is centered at transform.position
    40.             Vector2 testPosition = new Vector2(targetPosition.x, bottom.y);
    41.  
    42.             // Find how many pixels below us the land is, which should be zero.
    43.             int toLand = -GetPixelCountToLand(testPosition);
    44.             targetPosition.y += toLand;
    45.  
    46.             // If we are not needing to move down, make sure we don't need to move up.
    47.             if(toLand == 0)
    48.             {
    49.                 int toHover = GetPixelCountToHover(testPosition);
    50.                 targetPosition.y += toHover;
    51.             }
    52.         }
    53.         transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * 50);
    54.     }
    55. }

    The two functions for finding the distance to the land and out of the land are below.

    Code (CSharp):
    1. /// <summary>
    2. /// Determines how many pixels "in the ground" we are, and returns the amount of
    3. /// pixels that we need to move up to be on-top of the ground.
    4. /// </summary>
    5. /// <param name="position">The position to check against.</param>
    6. /// <returns></returns>
    7. private int GetPixelCountToHover(Vector2 position)
    8. {
    9.     bool opaque = false;
    10.     int count = 0;
    11.     while (opaque == false)
    12.     {
    13.         if (surface.InBounds(position) == false) return 0;
    14.         if (surface.GetPixel(position).a < 0.1)
    15.         {
    16.             opaque = true;
    17.             return count;
    18.         }
    19.         else
    20.         {
    21.             count++;
    22.             position.y = position.y + 1;
    23.         }
    24.     }
    25.  
    26.     return count;
    27. }
    28.  
    29. /// <summary>
    30. /// Determines how many pixels "above the ground" we are, and returns the amount of
    31. /// pixels that we need to move down to be on-top of the ground.
    32. /// </summary>
    33. /// <param name="position">The position to check against.</param>
    34. /// <returns></returns>
    35. private int GetPixelCountToLand(Vector2 position)
    36. {
    37.     bool opaque = false;
    38.     int count = 0;
    39.     while (opaque == false)
    40.     {
    41.         if (count >= 5) return 5;
    42.         if (surface.InBounds(position) == false) return 0;
    43.         if (surface.GetPixel(position).a >= 0.1)
    44.         {
    45.             opaque = true;
    46.             return count;
    47.         }
    48.         else
    49.         {
    50.             count++;
    51.             position.y = position.y - 1;
    52.         }
    53.     }
    54.  
    55.     return count;
    56. }

    The player has a BoxCollider2D on it, which is how I generate the "Rays" that do pixel detection for determining if the player is grounded or now. This is what defined the "bottom" variable, seen while creating the testPosition variable.

    Code (CSharp):
    1. bottom = new Vector2(collider.bounds.min.x + ((collider.bounds.max.x - collider.bounds.min.x) / 2), collider.bounds.min.y);
    This is still really rough as you can see: (It's worse outside of the gif, the low framerate almost makes it look decent lol)





    Here's the full code, if interested:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using PixSurf;
    5.  
    6. [RequireComponent(typeof(BoxCollider2D))]
    7. public class PlayerController : MonoBehaviour
    8. {
    9.     private PixelSurface surface;
    10.     private BoxCollider2D collider;
    11.  
    12.     private Vector2 bottomLeft = Vector2.zero;
    13.     private Vector2 bottomRight = Vector2.zero;
    14.     private Vector2 right = Vector2.zero;
    15.     private Vector2 left = Vector2.zero;
    16.     private Vector2 topLeft = Vector2.zero;
    17.     private Vector2 topRight = Vector2.zero;
    18.     private Vector2 top = Vector2.zero;
    19.     private Vector2 bottom = Vector2.zero;
    20.  
    21.     private bool grounded = false;
    22.     private bool blockedLeft = false;
    23.     private bool blockedRight = false;
    24.     private bool blockedTop = false;
    25.  
    26.     private Vector2 velocity = Vector2.zero;
    27.     private float movementSpeed = 10f;
    28.     private float movementSlope = 7f;
    29.  
    30.     private bool activeEntity = true;
    31.  
    32.     private Dictionary<string, bool> inputs = new Dictionary<string, bool>
    33.     {
    34.         { "left", false },
    35.         { "right", false },
    36.         { "jump", false }
    37.     };
    38.  
    39.  
    40.     private void Start()
    41.     {
    42.         surface = GameObject.Find("MAP").GetComponent<PixelSurface>();
    43.         collider = GetComponent<BoxCollider2D>();
    44.     }
    45.  
    46. private void Update()
    47. {
    48.     velocity = Vector2.zero;
    49.  
    50.     if (activeEntity)
    51.     {
    52.         ResetFlags();
    53.         SetRaycastPositions();
    54.         ProcessRays();
    55.          
    56.         if (inputs["right"] && blockedRight == false)
    57.         {
    58.             velocity.x = movementSpeed / 10;
    59.         }
    60.  
    61.         if (inputs["left"] && blockedLeft == false)
    62.         {
    63.             velocity.x = -(movementSpeed / 10);
    64.         }
    65.  
    66.     }
    67.  
    68.     if(grounded == false)
    69.     {
    70.         velocity.y = -GetPixelCountToLand(bottom);
    71.     }
    72.    
    73.     if (velocity != Vector2.zero)
    74.     {
    75.         // Get the position that we're trying to move to.
    76.         Vector2 targetPosition = (Vector2)transform.position + velocity;
    77.         if (velocity.y == 0 && velocity.x != 0)
    78.         {
    79.             // Get the difference between our position and the target position.
    80.             int xDif = (int)(transform.position.x - targetPosition.x);
    81.  
    82.             // Generate the test position.
    83.             // NOTE: bottom.y is the bottom position of the Player, and is what connects
    84.             //       the player to the ground, as the sprite is centered at transform.position
    85.             Vector2 testPosition = new Vector2(targetPosition.x, bottom.y);
    86.  
    87.             // Find how many pixels below us the land is, which should be zero.
    88.             int toLand = -GetPixelCountToLand(testPosition);
    89.             targetPosition.y += toLand;
    90.  
    91.             // If we are not needing to move down, make sure we don't need to move up.
    92.             if(toLand == 0)
    93.             {
    94.                 int toHover = GetPixelCountToHover(testPosition);
    95.                 targetPosition.y += toHover;
    96.             }
    97.         }
    98.         transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * 50);
    99.     }
    100. }
    101.  
    102.     private void ResetFlags()
    103.     {
    104.         grounded = false;
    105.         blockedLeft = false;
    106.         blockedRight = false;
    107.         blockedTop = false;
    108.     }
    109.  
    110.     private void SetRaycastPositions()
    111.     {
    112.         bottomLeft = new Vector2(collider.bounds.min.x, collider.bounds.min.y + movementSlope);
    113.         bottomRight = new Vector2(collider.bounds.max.x, collider.bounds.min.y + movementSlope);
    114.  
    115.         topLeft = new Vector2(collider.bounds.min.x, collider.bounds.max.y);
    116.         topRight = new Vector2(collider.bounds.max.x, collider.bounds.max.y);
    117.  
    118.         left = new Vector2(collider.bounds.min.x, collider.bounds.min.y + ((collider.bounds.max.y - collider.bounds.min.y) / 2));
    119.         right = new Vector2(collider.bounds.max.x, collider.bounds.min.y + ((collider.bounds.max.y - collider.bounds.min.y) / 2));
    120.  
    121.         top = new Vector2(collider.bounds.min.x + ((collider.bounds.max.x - collider.bounds.min.x) / 2), collider.bounds.max.y);
    122.         bottom = new Vector2(collider.bounds.min.x + ((collider.bounds.max.x - collider.bounds.min.x) / 2), collider.bounds.min.y);
    123.     }
    124.  
    125.     private void ProcessRays()
    126.     {
    127.         float speed = movementSpeed + 1;
    128.  
    129.         if (IsBlocked(topRight, true, true, speed)) blockedRight = true;
    130.         if (blockedRight == false && IsBlocked(right, true, true, speed)) blockedRight = true;
    131.         if (blockedRight == false && IsBlocked(bottomRight, true, true, speed / 3, true)) blockedRight = true;
    132.  
    133.         if (IsBlocked(topLeft, true, false, speed)) blockedLeft = true;
    134.         if (blockedLeft == false && IsBlocked(left, true, false, speed)) blockedLeft = true;
    135.         if (blockedLeft == false && IsBlocked(bottomLeft, true, false, speed / 3)) blockedLeft = true;
    136.  
    137.         if (IsBlocked(bottom, false, false, 4f)) grounded = true;
    138.         if (grounded == false && IsBlocked(bottomLeft, false, false, 4f)) grounded = true;
    139.         if (grounded == false && IsBlocked(bottomRight, false, false, 4f)) grounded = true;
    140.  
    141.         if(grounded)
    142.         {
    143.             if (IsBlocked(top, false, true, 5f)) blockedTop = true;
    144.             if (blockedTop == false && IsBlocked(topLeft, false, true, 5f)) blockedTop = true;
    145.             if (blockedTop == false && IsBlocked(topRight, false, true, 5f)) blockedTop = true;
    146.         }
    147.     }
    148.  
    149.     /// <summary>
    150.     /// Determines how many pixels "in the ground" we are, and returns the amount of
    151.     /// pixels that we need to move up to be on-top of the ground.
    152.     /// </summary>
    153.     /// <param name="position">The position to check against.</param>
    154.     /// <returns></returns>
    155.     private int GetPixelCountToHover(Vector2 position)
    156.     {
    157.         bool opaque = false;
    158.         int count = 0;
    159.         while (opaque == false)
    160.         {
    161.             if (surface.InBounds(position) == false) return 0;
    162.             if (surface.GetPixel(position).a < 0.1)
    163.             {
    164.                 opaque = true;
    165.                 return count;
    166.             }
    167.             else
    168.             {
    169.                 count++;
    170.                 position.y = position.y + 1;
    171.             }
    172.         }
    173.  
    174.         return count;
    175.     }
    176.  
    177.     /// <summary>
    178.     /// Determines how many pixels "above the ground" we are, and returns the amount of
    179.     /// pixels that we need to move down to be on-top of the ground.
    180.     /// </summary>
    181.     /// <param name="position">The position to check against.</param>
    182.     /// <returns></returns>
    183.     private int GetPixelCountToLand(Vector2 position)
    184.     {
    185.         bool opaque = false;
    186.         int count = 0;
    187.         while (opaque == false)
    188.         {
    189.             if (count >= 5) return 5;
    190.             if (surface.InBounds(position) == false) return 0;
    191.             if (surface.GetPixel(position).a >= 0.1)
    192.             {
    193.                 opaque = true;
    194.                 return count;
    195.             }
    196.             else
    197.             {
    198.                 count++;
    199.                 position.y = position.y - 1;
    200.             }
    201.         }
    202.  
    203.         return count;
    204.     }
    205.  
    206.     private bool IsBlocked(Vector2 start, bool horizontal, bool increment, float distance, bool debug = false)
    207.     {
    208.  
    209.         if (horizontal)
    210.         {
    211.             int x = (int)start.x;
    212.             int final = increment ? (int)start.x + (int)distance : (int)start.x - (int)distance;
    213.             while(x != final)
    214.             {
    215.                 if (surface.GetPixel(x, (int)start.y).a >= 0.1)
    216.                 {
    217.                     Debug.DrawRay(start, new Vector2(increment ? distance : -distance, 0f), Color.red, 0.1f);
    218.                     return true;
    219.                 }
    220.  
    221.                 if (increment) x++;
    222.                 else x--;
    223.             }
    224.        
    225.         }
    226.         else
    227.         {
    228.             int y = (int)start.y;
    229.             int final = increment ? (int)start.y + (int)distance : (int)start.y - (int)distance;
    230.             while(y != final)
    231.             {
    232.                 if (surface.GetPixel((int)start.x, y).a >= 0.1)
    233.                 {
    234.                     Debug.DrawRay(start, new Vector2(0f, distance), Color.red, 0.1f);
    235.                     return true;
    236.                 }
    237.  
    238.                 if (increment) y++;
    239.                 else y--;
    240.             }
    241.         }
    242.  
    243.         Debug.DrawRay(start, new Vector2(horizontal ? increment ? distance : -distance : 0f, horizontal ? 0f : increment ? distance : -distance), Color.red, 0.1f);
    244.         return false;
    245.     }
    246.  
    247.     public void SetInput(string key, bool value)
    248.     {
    249.         inputs[key] = value;
    250.     }
    251.  
    252.     public bool IsGrounded()
    253.     {
    254.         return grounded;
    255.     }
    256.  
    257.     public bool IsBlocked(string direction)
    258.     {
    259.         switch(direction)
    260.         {
    261.             case "left": return blockedLeft;
    262.             case "right": return blockedRight;
    263.             case "top": return blockedTop;
    264.             default: return false;
    265.         }
    266.     }
    267. }
    268.  
    269.  
     
  13. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Let's take this to private conversation (or start a separate thread about your project) so we don't fill up this thread too much.
     
  14. Dark_Seth

    Dark_Seth

    Joined:
    May 28, 2014
    Posts:
    140
    Just want to drop a Video here what I have managed to do with this. But stuck on ideas but it looks good.
    Managed to work in a good 2d Light and Weather system too. All showed in the Video

    Be Good , Be Kind. Cheers.

     
    MD_Reptile, JoeStrout and mgear like this.
  15. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Wow, that looks amazing! I love how the mining beam (if that's what it is). Beautiful job with the parallax scrolling, too. And the sky and weather affects blow me away.

    If you're stuck on where to go with this next, I suggest you post this to the Game Design subforum. Folks there will eat this up. You could even make a WebGL demo and post it to the next Feedback Friday. I bet you'll get lots of good ideas.

    I really encourage you to finish this game. You have created a beautiful world I want to spend time in!
     
    Dark_Seth likes this.
  16. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    @Dark_Seth That looks great! Fantastic work, I am impressed for sure.

    I'd say if your a bit hung up on where to go next with it - AI for sure! Get some enemies who can navigate with some A-Star like system and you will be golden.
     
    Dark_Seth likes this.
  17. Dark_Seth

    Dark_Seth

    Joined:
    May 28, 2014
    Posts:
    140
    Hi.
    Thanks for the reply
    I got a A* star ( made one a while back that I can plug into any project 3d or 2d. Have units script for ground and air units. )
    I more thinking of Gameplay / Objectives at this stage.

    Regards
     
    MD_Reptile likes this.
  18. SirDifferential

    SirDifferential

    Joined:
    Feb 15, 2020
    Posts:
    1
    There's a type downcasting bug in PixelSurface::CreateTiles()

    Code (CSharp):
    1.  
    2. void CreateTiles() {
    3.            if (shader == null) shader = Shader.Find("Sprites/Default");
    4.            int tileCols = Mathf.CeilToInt(totalWidth / tileWidth);
    5.            int tileRows = Mathf.CeilToInt(totalHeight / tileHeight);
    6.            tileObj = new GameObject[tileCols, tileRows];
    7.            tileTex = new Texture2D[tileCols, tileRows];
    8.            tileNeedsApply = new bool[tileCols, tileRows];
    9.           ....
    10.  
    This needs to be:

    Code (CSharp):
    1.  
    2.            int tileCols = Mathf.CeilToInt(totalWidth / (float)tileWidth);
    3.            int tileRows = Mathf.CeilToInt(totalHeight / (float)tileHeight);
    4.  
    This bug can cause an out-of-bounds memory access for specific sizes such as 1920x1080.
     
    JoeStrout likes this.
  19. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Great catch, @SirDifferential! It also occurs in the Clear() method. This will only affect people whose tile size does not divide evenly into the total size, but if that's you, please apply these changes.

    I have a few other fixes and enhancements to PixelSurface sitting on my hard drive (it's been getting a lot of exercise here thanks to Mini Micro). I plan to pack those up and push an update to the Asset Store in the next month or two, and when I do, it will certainly include this fix.
     
  20. mtoman

    mtoman

    Joined:
    Sep 17, 2015
    Posts:
    4
    Hi, you probably already got this one:
    FillEllipse: when calculating cx the sqrt becomes NaN for the bottom-most y. Resulting in a single pixel drawn at x=0 of the surface..
     
    MD_Reptile and JoeStrout like this.
  21. teevik

    teevik

    Joined:
    Sep 3, 2019
    Posts:
    3
    Hey, really big fan of this asset @JoeStrout!

    I'm also looking to simulate water, is there any good techniques?

    Would it make sense to do cellular automata not by iterating over the whole grid, but instead having a live pixel that checks the neighbors, and transfers water to them if they exist, or creates new water live pixels if they don't?
     
    JoeStrout likes this.
  22. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Yes, that's usually a better approach than doing CA over the whole grid (unless your grid is very small).

    But water simulation in general is a rather difficult topic. After trying a few different things, here's the approach I would currently use:
    1. Find completely connected sections of water. (You could do this with a flood fill algorithm for example.) Keep track of these in some efficient way.
    2. Now, on each update:
      1. Find the highest water pixel in the whole section.
      2. Move it to the lowest empty neighbor of any water pixel in the whole section.
    This will rapidly move the whole section down and flatten it out in what I think would be a very water-like way. It will even rapidly equalize around a U-bend, which other approaches take a long time to do.

    Or so I imagine. You should try it and let us know how it goes. :)
     
    teevik likes this.
  23. teevik

    teevik

    Joined:
    Sep 3, 2019
    Posts:
    3


    I think i got it working. I'll need to work on the performance though. Atm flood fill is using 40% of cpu usage, since i'm not caching it anywhere

    I'd also need some way to let the water settle, so it won't update every frame

    (Also, i'm using a heavily modified version of pixelsurface, so i should probably try the normal one as well)
     
    JoeStrout likes this.
  24. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Looks great though! Very watery indeed! :D
     
  25. mtoman

    mtoman

    Joined:
    Sep 17, 2015
    Posts:
    4
    Hi,
    I've played some time with your Asset now, mostly to build an interactive painting app for my daughter.
    What I had in mind was actually very similar to your demo scene.

    Well, most of my game dev experience is from when you wrote data in some buffer and then dumped it into the video buffer every frame ;).
    So what really tripped me up is how slow uploading the modified textures to the GPU really is.

    First I used a resolution of 1920x1080 as target, soon halfed it. But when you pump up the snow it really slows down to crawl. At first I planned to add a simpler live pixel without the stacks etc and instead just a vector of plain coordinates (probably in data oriented style) but soon realized that this isnt the bottleneck.

    Just disabling all live pixels but running Apply for all tiles every frame slows things down to 10fps on my dev machine. Devices like the surface go are completely unusable at that point.

    I've tried different tile sizes from 16x16 to 128x128 but didn't have much luck.

    Reduced the overall resolution of the surface to some oldschool DOS times resolutions ;). Sped things up but on FHD and more I quickly get ugly upscaling artefacts on ellipses.

    I've tried alternatives like blocks (Terrain engine 2D) or drawing meshes but I found I really like the style I get with Pixelsurface.

    So I wondered if anyone got a hint for me for fitting resolutions, tile sizes, other tricks?

    Sorry I got no screenshot atm but just some live action photo ;)
    At this level the snow is really great but we're close to refreshing all tiles every frame.
     

    Attached Files:

  26. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Yep, you're right, the bottleneck is shoving the data over to the GPU. Tiling helps, but only if it means you're not updating every tile every frame; if you are, then it's really not going to help.

    Systems vary greatly in their CPU-to-GPU bandwidth, so on another machine a scene like this might get fine performance even at full resolution. On a lighter-weight machine, I think your only option is to turn down the resolution, or slow down the snow. For example, I wonder if you picked just one tile to update each frame, and only moved the snow in that tile, if it would be noticeable? Assuming you have (say) 16 tiles, you'd still be updating each tile every 16 frames, so if the snow's not moving too fast and you're able to maintain a good framerate this way, perhaps your brain would blend all the motion together?
     
  27. mtoman

    mtoman

    Joined:
    Sep 17, 2015
    Posts:
    4
    Thanks, I also thought about moving them 2 Pixels every second Update.
    Or generating thicker flakes of multiple pixels would probably also be a good solution ;).

    I'm not completely sure if it's possible or would make it any faster... do you think writing a custom fragment shader that draws live pixels could help?
    The problem is that it would either need a texture with the snow flakes, ending up with the same (or actually, worse) performance, or some list of positions, which every shader would have to search then.
    Or generate a fake mesh and only color the vertex positons. There a probably other ideas I don't think about right now.

    Other thing I wonder: why exactly do you need the LivePixelStacks? Just to make sure the last pixel that moved to the current position is drawn on top or do I miss anything? Or to avoid multiple calls to SetPixel of the texture (if you just looped through the list of live pixels which changed position in this frame and drew them)
     
    Last edited: Apr 13, 2020
  28. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Well the other thing you might do is use a particle system. These work with a quad per particle (snowflake), all showing the same texture, so you don't have the texture bottleneck. But of course you're no longer working with pixels in that case.

    The LivePixelStacks are an efficient way to manage "live" pixels that move around and may end up temporarily in the same place for a frame or two, and also ensures that they don't (permanently) clobber the background color. You don't have to use them; Mini Micro, for example, uses PixelSurface for its pixel displays, but does not make use of LivePixels.
     
  29. mtoman

    mtoman

    Joined:
    Sep 17, 2015
    Posts:
    4
    Thanks, yes at the moment I would not see a huge issue having no Stacks as the positions of all Live Pixels are known anyway, so no information lost there. And once a live pixel overwrites a non-live pixel it's probably OK when it's gone forever. Although I'd also prefer to work with two buffers in that case.

    But as managing those lists isn't the bottleneck at the moment anyway, it's not a real priority (except if you burn down half the screen).

    I tried skipping frames but it felt weird, even when they moved twice the distance per update it felt like a slow-mo effect ;).
    Now set it to 768x384 which still looks fine on a 14 inch 16:9 and gives good performance. Will have to try the Surface Go later as a rather low-end device (but with pen).
    On my dev notebook 384x384 for the tiles actually gives the best performance (so just 2 tiles) and I don't have to care about keeping live pixel confined to smaller regions. but I suspect this will be radically different on other devices.
    And have to check if I don't get upscaling artefacts on the 3:2, 2,736 × 1,824 surface go.

    Still, working with individual pixels is great as every ;). Never really joined Team Triangle ;)
     
    JoeStrout likes this.
  30. Geopi

    Geopi

    Joined:
    Aug 18, 2018
    Posts:
    3
    This asset is wonderful, performances are incredible. For those who are looking for an advanced fluid simulation for water / lava petroleum etc check this http://www.jgallant.com/2d-liquid-simulator-with-cellular-automaton-in-unity/
    it is very simple and It didnt take me much time to change it to fit to this code. Instead of changing the sprite you just change the color based on the liquid amount. I just made sure to add a lateupdate in addition to the update in order to make the changing of pressure after each cells have proceeded. Also, using such liquid simulator prevent stray pixels sliding on the surface AND YOU CAN MAKE VOLCANOES , TSUNAMI ...MOUHAHAHA
    Surprisingly on my potato computer even by having 1000s of lava pixel that has a temperature transmission algo and reaction to wood / water the game ran very smoothly with NO optimisation. (delayed updates / pixel settling / caching)

    I wish I could integrate it easily with this asset tho : https://assetstore.unity.com/packages/tools/sprite-management/destructible-2d-18125

    test.png
     
    MD_Reptile and JoeStrout like this.
  31. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    One of our users has pointed out a flaw in the published version of PixelSurface: mip maps are turned on, which can cause the surface to appear blurry/compressed under some circumstances.

    The fix is to change the that creates each tile, in the CreateTiles() method, to:

    Code (CSharp):
    1.     Texture2D tex = new Texture2D(tileWidth, tileHeight, TextureFormat.ARGB32, false);
    The most important bit being the last parameter (false), which tells Unity not to create mip maps. You might want to also do the same in the GetTexture method if you are using that.

    I plan to submit a PixelSurface update with this and other small improvements in about a month, but I thought I should notify everyone about this one now.
     
    MD_Reptile likes this.
  32. KrisGungrounds

    KrisGungrounds

    Joined:
    Aug 20, 2019
    Posts:
    14
    Hi, I bought your asset, it's perfect for what I need! Great job! It looks great, feels great, and it has amazing performance.

    It seems I have to rewrite some core logic though, to make this work more similar to Powder Toy.

    Few examples:
    1) I want that when the sand hits the water, it can drown inside of the water. This means that not only the sand needs to keep going, but the surrounding water needs to be updated as well. To make this work, it seems I will have to get surrounding "dead pixels" and convert them back to living pixels. But the thing is, I will need to know the type because just checking the color isn't reliable enough.

    What is your suggestion, how should I handle this? How can I get the behavior that's more close to Powder Toy? Is there any collection that keeps track of all dead pixels, or all checks are handled through Texture2D?

    2) Following this, in various instances, it's not enough to check the collision between the dead and live pixels, but the collision between live pixels has to be checked as well. But to make this work, the Update of all live pixels has to be ordered. Live pixels should be updated from the bottom left towards the top right. Explanation here:



    3) Do you maybe have an algorithm for getting all connected dead pixels? That way, if there is an explosion below the water, I could convert all connected dead pixels into live pixels.

    4) There is one more thing related to collision between dead and live pixels, when the explosion happens, even if the chance to convert dead pixel to live pixel is 100%, the resulting surface size on the ground is not equal to the surface that fell.

    Thanks for the awesome asset! I have big plans with this, hopefully, these problems are solvable.

    Cheers! :)

    EDIT:

    Following the video above, I tried to recreate their water, and I think it's pretty good result, though it has some problems. I added the random check and made it go into the chosen direction because otherwise, all water would just keep going left and right, and it would never become dead pixel. This is because live pixels don't interact with each other.

    Here is how it looks like, plus I also added the code below. Note, I disabled the gravity and drag, so the code might need some modifications if you want to use it with PixParticle.



    Code (CSharp):
    1.    public override void UpdateColission(PixelSurface surf)
    2.     {
    3.         if (ClearAt(surf, x, y - 1))
    4.         {
    5.             position.y--;
    6.             selectedDirection = false;
    7.         }
    8.         else if (ClearAt(surf, x - 1, y - 1))
    9.         {
    10.             position.x--;
    11.             position.y--;
    12.             selectedDirection = false;
    13.         }
    14.         else if (ClearAt(surf, x + 1, y - 1))
    15.         {
    16.             position.x++;
    17.             position.y--;
    18.             selectedDirection = false;
    19.         }
    20.         else
    21.         {
    22.             if (selectedDirection)
    23.             {
    24.                 if (ClearAt(surf, x + dir, y))
    25.                 {
    26.                     position.x += dir;
    27.                 }
    28.                 else
    29.                 {
    30.                     Die();
    31.                 }
    32.             }
    33.             else
    34.             {
    35.                 selectedDirection = true;
    36.                 bool clearRight = ClearAt(surf, x + 1, y);
    37.                 bool clearLeft = ClearAt(surf, x - 1, y);
    38.                 if (clearLeft && clearRight) dir = Random.value > 0.5f ? 1 : -1;
    39.                 else if (clearLeft) dir = -1;
    40.                 else if (clearRight) dir = 1;
    41.            
    42.                 if(clearLeft || clearRight)
    43.                 {
    44.                     position.x += dir;
    45.                 }
    46.                 else
    47.                 {
    48.                     Die();
    49.                 }
    50.             }
    51.         }
    52.         velocity = Vector2.zero;
    53.     }
     
    Last edited: Aug 24, 2020
    JoeStrout likes this.
  33. unity_h8aov5DfFqnsXQ

    unity_h8aov5DfFqnsXQ

    Joined:
    Apr 9, 2019
    Posts:
    1
    Hello! what is the best way to create nondestructible pixels for you? With my solution, indestructible pixels do not collide with other pixels.
     
  34. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    I've never used Powder Toy, so I can't comment on any similarities or differences with that.

    It should be enough. You have 2^24 different colors to work with, not counting alpha; surely you can pick a unique one to mean "water"? This is the standard PixelSurface approach anyway, because to keep them alive would mean literally thousands of little objects, which would kill performance.

    But if you really can't use the color, then I think you will need to keep track of the pixel types yourself, say in a big 2D array of byte, where the value at each point indicates the type.

    Within PixelSurface, dead pixels are just colors in a Texture2D.

    That would be true for some uses but not others. To do that efficiently you will need either a sparse matrix data structure, or (more likely) decide to sacrifice memory for performance, and keep a giant 2D array of live pixel stacks. You can see how this is not something appropriate for PixelSurface in general, as it would be quite a large cost (especially on mobile or memory-constrained systems) that provides no benefit to many users.

    Yep, you want a standard flood-fill algorithm here. I can provide more details if that doesn't ring a bell.

    I'm not sure quite what you mean here. Perhaps you can provide a simple example?

    I think it's a great result! Getting convincing, high-performance water is not easy — if you look up in the thread, there have been a few prior attempts, but I think yours is the best I've seen. I'm going to have to study what you did here!

    Thanks for sharing, and for using PixelSurface!
     
    KrisGungrounds likes this.
  35. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    I'm not sure what you mean. PixelSurface itself has no concept of "destructible" or "indestructible"; this is something you code into your own logic. The demo does some explosions and whatnot, but that's just a demo, not part of the core asset.

    In games where I've done something like this for real, I simply have a function that figures out whether a pixel is destructible or not based on the color — e.g., I have a set of "metal" colors that can't be destroyed, and everything else can; or any color whose blue component (as an integer) is even is destructible, and if it's odd it is indestructible.
     
  36. KrisGungrounds

    KrisGungrounds

    Joined:
    Aug 20, 2019
    Posts:
    14
    Thank you for the detailed and quick response JoeStrout!

    Oh! I really thought that was the inspiration. :D If you are interested to see where the insanity can go, check this out:


    Yeah, what I am worried about is if I import complex terrain images, some pixels on it could have the color that's shared with a live pixel class. So, once I would explode that terrain, it would create some crazy results. But maybe that's part of the fun? :)

    Yeah, it makes sense why this wasn't included. Though, I got pretty good results even when I changed that the pixels cannot die at all. The game was running above 100 FPS, in editor, with around 20 000 live pixels on the screen. Really great job there!

    If you maybe have it somewhere, written in C#, it would help, otherwise, I will probably find something.

    Imagine if you had a rectangle of dead pixels. Rectangle's size is 100x100. You turn them all into living pixels, and they start to fall. When they crash the ground, I think this is what happens:
    - live pixel enters dead pixel, it moves one up
    - but at the position when it moved, there was another live pixel, which in the next frame will enter the same dead pixel from before
    - the code will move it just 1 up instead of 2

    When this happens, the ground area will not be filled with 10 000 pixels because many dead pixels will overlap.


    Thanks, glad you like it! So, while your sand checks can it move left or right at the y position of the dead pixel that it hits, I also check can it move left or right above that dead pixel. Because water is like that, it slides around. There is a way to improve it, and that is to use random direction instead of having defined direction, but in order to make that really work, I think these water pixels have to collide with each other. Otherwise, what happens with the current code is that all water becomes one line of pixels at the bottom. The chance of them dying is really low because the only way they can die is by falling into a 1-pixel wide hole.

    Also, I did turn off the gravity and drag, but that probably wasn't needed. I will work more on this, so I will come back with more info once I get the facts straight. :)
     
    Last edited: Aug 25, 2020
  37. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    That's great!

    (Re. flood fill...)

    Well in fact there is a FloodFill implementation in PixelSurface itself (around line 600 in my current version). Looking at that should give you the idea.

    The only difference is, I'm making use of a trick: I know I'm changing the color, so I don't have to worry about processing the same pixel more than once. If you're just gathering up all the connected pixels, but not changing their color at the same time, then you will need some other way to avoid processing points you've already processed. A Dictionary might do.

    Ah, I see. Yes, my demo code was not meant to display conservation of matter. What you're describing could indeed happen.

    If that's a problem in your game, then you'll need to do something more sophisticated, making sure that every live pixel finds an "empty" space before it plants itself back down.

    Yes, but that's not enough to make a realistic water simulation. The trick with that comes when you have something like a U-bend: water should maintain the same level on both sides of the bend, even as you add water to just one side.

    The best approach I've seen for that is to, on every turn, (1) find the highest pixel in the connected set, and (2) move to the lowest empty space next to the set. This will rapidly level out the water. But it's still hard to do efficiently on a large scale.

    But I confess I haven't had a chance yet to watch that video you linked. Maybe they've got a better approach!
     
    KrisGungrounds likes this.
  38. KrisGungrounds

    KrisGungrounds

    Joined:
    Aug 20, 2019
    Posts:
    14
    I tested the fluids in Powder Toy, but they didn't solve the U-Fluid problem. I will try to implement it tomorrow, and if I succeed, I will also post the code here.

    I noticed that GetPixel can also check live pixels! That's exactly what I need, sometimes I have to check the collision between two live pixels. I was looking at the code, but I am not sure what exactly is happening here.

    After you check the stackMap, if the pixel is not inside of it, you continue to do additional checks.

    Can you please tell me more details about how this method works? Thanks!
     
  39. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Sure. The point of this method is to get the color of the given pixel, but what that means may vary with the situation. If includeLive is true, then (as you noticed) it first checks live pixels, which are always drawn on top; the topmost live pixel is the color of that pixel. If there aren't any live pixels there, or if the caller wants us to ignore live pixels, then the color is whatever color is stored in the texture map.
     
    KrisGungrounds likes this.
  40. Dark_Seth

    Dark_Seth

    Joined:
    May 28, 2014
    Posts:
    140
    @JoeStrout

    Any idea how I can paint another image onto the pixelsurface. Like a stamp of an tree or a ball. Each pixel painted keeps its color. I know your example has fill etc. just can't firgure it out.
     
  41. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    There is a DrawTexture method for exactly that. There's even DrawTextureWithColor if you want to tint it as you draw.

    If you use this and find it's too slow, don't give up on it — I've done some fairly significant optimizations to PixelSurface because of Mini Micro (which uses it for the pixel display layers). Packing up a PixelSurface update is on my to-do list, but if you need it, I'll be happy to send you a copy privately (which is honestly a lot easier to do than futzing about with the Asset Store).
     
  42. Dark_Seth

    Dark_Seth

    Joined:
    May 28, 2014
    Posts:
    140

    PM sent. Thanks man!
     
  43. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    EDIT: My own mistake with the first question I posted here, I figured out that it does seem to work with TimeScale :)

    I̶ ̶w̶a̶s̶ ̶t̶i̶n̶k̶e̶r̶i̶n̶g̶ ̶w̶i̶t̶h̶ ̶P̶i̶x̶e̶l̶ ̶S̶u̶r̶f̶a̶c̶e̶ ̶a̶g̶a̶i̶n̶ ̶t̶o̶d̶a̶y̶,̶ ̶a̶n̶d̶ ̶d̶e̶c̶i̶d̶e̶d̶ ̶t̶o̶ ̶t̶r̶y̶ ̶s̶o̶m̶e̶ ̶s̶l̶o̶w̶ ̶m̶o̶t̶i̶o̶n̶ ̶b̶y̶ ̶a̶d̶j̶u̶s̶t̶i̶n̶g̶ ̶t̶h̶e̶ ̶t̶i̶m̶e̶s̶c̶a̶l̶e̶ ̶-̶ ̶t̶h̶i̶n̶k̶i̶n̶g̶ ̶i̶t̶ ̶m̶i̶g̶h̶t̶ ̶b̶e̶ ̶a̶s̶ ̶s̶i̶m̶p̶l̶e̶ ̶a̶s̶ ̶t̶h̶a̶t̶,̶ ̶b̶u̶t̶ ̶u̶n̶f̶o̶r̶t̶u̶n̶a̶t̶e̶l̶y̶ ̶i̶t̶ ̶d̶o̶e̶s̶n̶'̶t̶ ̶s̶e̶e̶m̶ ̶t̶o̶ ̶w̶o̶r̶k̶ ̶o̶u̶t̶ ̶o̶f̶ ̶t̶h̶e̶ ̶b̶o̶x̶.̶ ̶I̶ ̶f̶i̶g̶u̶r̶e̶d̶ ̶I̶'̶d̶ ̶a̶s̶k̶ ̶b̶e̶f̶o̶r̶e̶ ̶I̶ ̶d̶i̶v̶e̶ ̶i̶n̶ ̶t̶o̶ ̶t̶h̶e̶ ̶g̶u̶t̶s̶ ̶o̶f̶ ̶P̶i̶x̶e̶l̶ ̶S̶u̶r̶f̶a̶c̶e̶ ̶i̶f̶ ̶t̶h̶e̶r̶e̶ ̶i̶s̶ ̶a̶ ̶b̶u̶i̶l̶t̶ ̶i̶n̶ ̶w̶a̶y̶ ̶t̶o̶ ̶c̶o̶n̶t̶r̶o̶l̶ ̶"̶t̶i̶m̶e̶"̶ ̶i̶n̶ ̶t̶h̶e̶ ̶f̶r̶a̶m̶e̶w̶o̶r̶k̶?̶

    Also when I imported the package the text labels on the demo scene UI had a font that was not included (or somehow lost its setting) and the labels were not visible until I set them to Arial - perhaps a font was removed in recent updates?

    Another question occured to me - when "exploding" pixels in the demo scene, if you rapidly click multiple times, the new "explosion" will erase the dynamic pixels of the previous explosion - is there a simple way to allow the old dynamic pixels to remain while "exploding" the ones that are still static?
     
    Last edited: Jun 17, 2021
  44. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Probably. I'm well overdue to post another Asset Store update; I'll be sure to check the fonts. Thank you for pointing this out.

    Yep. The GetPixel methods all take an "includeLive" parameter. If that is false, then the result will ignore any live (dynamic) pixels at that position, and just set the static color. Also, the SetPixel method only sets the static color, so together these will let you explode the static terrain and ignore the flying particles.
     
  45. graphicsayy97

    graphicsayy97

    Joined:
    Dec 24, 2020
    Posts:
    50
    Uhh, dev,are yoou still here? i will wire you(or someone) like 100$ to keep updating this

    im trying to recreate cortex command in unity (for myself not really just to sell...)
    and yeah the source code is out there, but I don't want to rip that off

    i LOVE pixel physic sand box but unity solution all end up being tied to texture renderer manipulation.. can be hard to keep track of it with input being on other data layer...
     
  46. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Of course I'm still here (my last post is above, literally a month ago). You don't need to wire any money. If you have a need, just ask. :)

    I'm not familiar with cortex command, but if it's a pixel-based game, PixelSurface is the way to go!
     
    graphicsayy97 likes this.
  47. graphicsayy97

    graphicsayy97

    Joined:
    Dec 24, 2020
    Posts:
    50
    Hello dev thanks you for your response

    so this is cortex command:



    i have thoughts on various aspects of implementation and I am currently looking over the source code of pixelsurface

    i was thinking, on ways to solve pathfinding, and I had some questions, if i want to check wether or not there is a lack of a "terrain pixel" in a certain region or square of arbitrary size, what would you do, iterate all pixels on rendered pixelsurface within this region? is there a way to freeze areas outside a region of view to make calculating live changes or pathing grid cheaper,

    also is there ways for infinite terrain? is the pixel surface just one obj? i guess i can make custom changes which just store my surfaces as binary or something and read them in my grid? i was hoping to make huuge maps loading in chunks of pixel surfaces for player to walk in.

    i will be experimenting on the pathfinding issue in the mean time using your asset, i know i can make this in SDL or SFML but I rather work in unity.
     
  48. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    At a basic level, yes. But an important optimization for something like this would be to have a higher-level grid or even quadtree keeping track of larger (probably square) areas of the map. Perhaps for each such larger cell you would keep track of whether it is (1) fully clear, (2) partially clear, or (3) completely blocked. Then you could path-find on these larger cells first, and switch to a finer-grained pathfinding within the cell when you get there.

    Well nothing changes pixels (or moves live pixels) except your code. So, this is again a higher-order thing you would just need to code up for your game, however you want to do it.

    As with any infinite terrain game, it's really a matter of having a bunch of chunks that you load and unload as needed. If you really mean "infinite" then it kind of implies you'll be procedurally generating those, too.

    All this is totally doable: I did exactly that for my game Rocket Plume. There were two PixelSurfaces, and whenever the lower one scrolled entirely off the screen I would move it up above the higher one (since in this game you can only move upward), and generate new obstacles.



    The PixelSurface is, yeah, though it has under it some number of tiles. You have control over the tile size; if you set it to the same size as the whole pixel surface, then there will be just one tile. But typically you set the tiles to something like 256x256, and then you have a number of these within a larger surface.

    Yep, you can do that.

    Best,
    - Joe
     
    graphicsayy97 likes this.
  49. MD_Reptile

    MD_Reptile

    Joined:
    Jan 19, 2012
    Posts:
    2,664
    This really is one the biggest hangups of making a CC clone - they did some smart work with the pathfinding that is a sort of variant of A star but that doesn't check paths at a per-pixel level. Instead they have some smart system in place where it would detect a certain sized cell of pixels (lets say 16 x 16 or so) and check if its "mostly open" to the point that a character should be able to step over or crawl through it, then that array of cells is only updated when something is damaged on the level, and the AI only does re-checks every so often to keep from bogging down the pathfinding.

    And what Joe mentions about using a grid of pixel surfaces will almost certainly be required unless you can live with smaller levels. In my own testing without using pixel surface the largest worlds I could optimally run on mobile were about 4096 x 4096 or some variant (2048 x 8192) of that size, but with clever optimizations you might be able to push beyond that.

    If you do manage to find an efficient way to do a game of that nature, I'd be curious how it turns out!
     
  50. graphicsayy97

    graphicsayy97

    Joined:
    Dec 24, 2020
    Posts:
    50
    great! I'm also surprised on the performance it top notch!!


    yes! this is the idea i have, same idea,

    if i uncover some good solution ill post code here will experiment
     
    MD_Reptile and JoeStrout like this.