├── .gitattributes ├── .gitignore ├── BinaryImage.cs ├── DestructibleSprite.cs ├── Projectile.cs ├── README.md └── ShootProjectile.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Ll]ibrary/ 2 | [Tt]emp/ 3 | [Oo]bj/ 4 | [Bb]uild/ 5 | 6 | # Autogenerated VS/MD solution and project files 7 | /*.csproj 8 | /*.unityproj 9 | /*.sln 10 | /*.suo 11 | /*.user 12 | /*.userprefs 13 | /*.pidb 14 | /*.booproj 15 | 16 | #Unity3D Generated File On Crash Reports 17 | sysinfo.txt 18 | 19 | # ========================= 20 | # Operating System Files 21 | # ========================= 22 | 23 | # OSX 24 | # ========================= 25 | 26 | .DS_Store 27 | .AppleDouble 28 | .LSOverride 29 | 30 | # Thumbnails 31 | ._* 32 | 33 | # Files that might appear on external disk 34 | .Spotlight-V100 35 | .Trashes 36 | 37 | # Directories potentially created on remote AFP share 38 | .AppleDB 39 | .AppleDesktop 40 | Network Trash Folder 41 | Temporary Items 42 | .apdisk 43 | 44 | # Windows 45 | # ========================= 46 | 47 | # Windows image file caches 48 | Thumbs.db 49 | ehthumbs.db 50 | 51 | # Folder config file 52 | Desktop.ini 53 | 54 | # Recycle Bin used on file shares 55 | $RECYCLE.BIN/ 56 | 57 | # Windows Installer files 58 | *.cab 59 | *.msi 60 | *.msm 61 | *.msp 62 | 63 | # Windows shortcuts 64 | *.lnk 65 | -------------------------------------------------------------------------------- /BinaryImage.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | public struct BinaryImage 4 | { 5 | public int x; // Width of the reference texture 6 | public int y; // Height of the reference texture 7 | private BitArray b; // BinaryImage of the reference texture 8 | public int Length; 9 | 10 | public BinaryImage(int x, int y) 11 | { 12 | this.x = x; 13 | this.y = y; 14 | 15 | b = new BitArray(x*y); 16 | Length = b.Length; 17 | } 18 | 19 | public BinaryImage(int x, int y, bool b) 20 | { 21 | this.x = x; 22 | this.y = y; 23 | 24 | this.b = new BitArray(x*y, b); 25 | Length = this.b.Length; 26 | } 27 | 28 | public void Set(int i, bool v) 29 | { 30 | b.Set(i, v); 31 | } 32 | 33 | public bool Get(int i) 34 | { 35 | return b.Get(i); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /DestructibleSprite.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System; 6 | 7 | [RequireComponent (typeof (PolygonCollider2D))] 8 | public class DestructibleSprite : MonoBehaviour { 9 | 10 | public Texture2D tex; 11 | 12 | // Testing 13 | public bool doInit = true; 14 | public bool doSplit = false; 15 | 16 | public bool doRefresh; 17 | public bool doBI; 18 | public bool doErosion; 19 | public bool doDilation; 20 | public bool doSub; 21 | public bool doSub2; 22 | public bool doVert; 23 | public bool doVertLong; 24 | public bool doComplete; 25 | 26 | public float pixelsToUnits = 100f; // Pixels to unity units 100 to 1 27 | public float pixelOffset = 0.5f; 28 | 29 | private BinaryImage binaryImage; 30 | 31 | PolygonCollider2D poly; 32 | 33 | public float xBounds; 34 | public float yBounds; 35 | 36 | public int islandCount=0; 37 | 38 | private List tempPath; 39 | private Vector2 endPoint; 40 | 41 | // Use this for initialization 42 | void Start () { 43 | 44 | poly = gameObject.GetComponent(); 45 | tex = Instantiate(gameObject.GetComponent().sprite.texture) as Texture2D; 46 | 47 | xBounds = gameObject.GetComponent().sprite.bounds.extents.x; 48 | yBounds = gameObject.GetComponent().sprite.bounds.extents.y; 49 | 50 | if(doInit) { 51 | binaryImage = BinaryImageFromTex(ref tex); 52 | binaryImage = tidyBinaryImage(binaryImage); 53 | updateCollider(); 54 | } 55 | } 56 | 57 | void Update() { 58 | 59 | if(doRefresh) { 60 | doRefresh = false; 61 | gameObject.GetComponent().sprite = Sprite.Create(tex, gameObject.GetComponent().sprite.rect, new Vector2(0.5f, 0.5f)); 62 | } 63 | 64 | if(doBI) { 65 | doBI=false; 66 | 67 | binaryImage = BinaryImageFromTex(ref tex); 68 | 69 | gameObject.GetComponent().sprite = Sprite.Create(BinaryImage2Texture(binaryImage), gameObject.GetComponent().sprite.rect, new Vector2(0.5f, 0.5f)); 70 | } 71 | 72 | if(doErosion) { 73 | doErosion=false; 74 | 75 | binaryImage = BinaryImageFromTex(ref tex); 76 | 77 | gameObject.GetComponent().sprite = Sprite.Create(BinaryImage2Texture(erosion(binaryImage)), gameObject.GetComponent().sprite.rect, new Vector2(0.5f, 0.5f)); 78 | } 79 | 80 | if(doDilation) { 81 | doDilation=false; 82 | 83 | binaryImage = BinaryImageFromTex(ref tex); 84 | 85 | gameObject.GetComponent().sprite = Sprite.Create(BinaryImage2Texture(dilation(binaryImage)), gameObject.GetComponent().sprite.rect, new Vector2(0.5f, 0.5f)); 86 | } 87 | 88 | if(doSub) { 89 | doSub=false; 90 | 91 | binaryImage = BinaryImageFromTex(ref tex); 92 | binaryImage = tidyBinaryImage(binaryImage); 93 | 94 | gameObject.GetComponent().sprite = Sprite.Create(BinaryImage2Texture(subtraction(binaryImage, erosion(binaryImage))), gameObject.GetComponent().sprite.rect, new Vector2(0.5f, 0.5f)); 95 | } 96 | 97 | if(doSub2) { 98 | doSub2=false; 99 | 100 | binaryImage = BinaryImageFromTex(ref tex); 101 | binaryImage = tidyBinaryImage(binaryImage); 102 | 103 | gameObject.GetComponent().sprite = Sprite.Create(BinaryImage2Texture(subtraction(binaryImage, erosion(binaryImage))), gameObject.GetComponent().sprite.rect, new Vector2(0.5f, 0.5f)); 104 | } 105 | 106 | if(doVert) { 107 | doVert=false; 108 | 109 | binaryImage = BinaryImageFromTex(ref tex); 110 | binaryImage = tidyBinaryImage(binaryImage); 111 | BinaryImage binaryImageOutline = subtraction(binaryImage, erosion(binaryImage)); 112 | List > paths = getPaths(ref binaryImageOutline); 113 | gameObject.GetComponent().sprite = Sprite.Create( BinaryImage2TextureUsingPaths(binaryImageOutline, paths), gameObject.GetComponent().sprite.rect, new Vector2(0.5f, 0.5f)); 114 | 115 | setCollider(ref paths); 116 | } 117 | 118 | if(doVertLong) { 119 | doVertLong=false; 120 | 121 | // TODO: simplify paths farther 122 | print("doVertLong has not been made yet :3"); 123 | 124 | gameObject.GetComponent().sprite = Sprite.Create(BinaryImage2Texture(binaryImage), gameObject.GetComponent().sprite.rect, new Vector2(0.5f, 0.5f)); 125 | } 126 | 127 | if(doComplete) { 128 | doComplete=false; 129 | 130 | binaryImage = BinaryImageFromTex(ref tex); 131 | binaryImage = tidyBinaryImage(binaryImage); 132 | updateCollider(); 133 | } 134 | } 135 | 136 | void split(BinaryImage b) { 137 | int startPos; 138 | 139 | // TODO: copy the binaryImage instead of setting it 140 | BinaryImage t = new BinaryImage(b.x, b.y); 141 | for(int x=0; x > islands = new List >(); 144 | 145 | // Find islands 146 | while(findStartPos(ref t, out startPos)) { 147 | List island = new List(); 148 | 149 | floodFill(ref t, ref island, startPos); 150 | 151 | islands.Add(island); 152 | } 153 | 154 | // If there is only 1 island we wont split anything 155 | if(islands.Count <= 1) return; 156 | 157 | // Get bounding boxes for each island 158 | for(int i=0; i x2) x2 = x; 168 | if(y < y1) y1 = y; 169 | else if(y > y2) y2 = y; 170 | } 171 | 172 | int w = x2-x1, h = y2-y1; // bounds 173 | int cx = (x2+x1)/2, cy = (y2+y1)/2; // new center for island 174 | 175 | // Create new gameobject 176 | GameObject go = new GameObject("DestructibleSpritePiece"); 177 | go.AddComponent(); 178 | go.AddComponent(); 179 | go.AddComponent(); 180 | go.GetComponent().doSplit = true; 181 | 182 | // Copy part of the original texture to our new texture 183 | Color32[] d = tex.GetPixels32(); 184 | Color32[] e = new Color32[w*h]; 185 | for(int x=0, y=0; x=x1 && x%tex.width=y1) { 187 | e[y] = d[x]; 188 | y++; 189 | } 190 | } 191 | 192 | // Apply to our new texture 193 | Texture2D texture = new Texture2D(w,h); 194 | texture.SetPixels32(e); 195 | texture.Apply(); 196 | 197 | // Add the spriteRenderer and apply the texture and inherit parent options 198 | SpriteRenderer s = go.GetComponent(); 199 | s.sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f)); 200 | s.color = gameObject.GetComponent().color; 201 | s.sortingOrder = gameObject.GetComponent().sortingOrder; 202 | 203 | // Set the position to the new center 204 | go.transform.position = new Vector3(transform.position.x + (cx + pixelOffset)/pixelsToUnits - xBounds, transform.position.y + (cy + pixelOffset)/pixelsToUnits - yBounds, transform.position.z); 205 | // Put it in the same layer as the parent 206 | go.layer = gameObject.layer; 207 | 208 | } 209 | // We can destroy the orignal object 210 | Destroy(gameObject); 211 | } 212 | 213 | void floodFill(ref BinaryImage b, ref List i, int pos) { 214 | int w = b.x; 215 | 216 | if(b.Get(pos)) { 217 | i.Add(pos); 218 | b.Set(pos, false); 219 | } else return; 220 | 221 | if((pos%w)+1 < w) floodFill(ref b, ref i, pos+1); // Right 222 | if((pos%w)-1 >= 0) floodFill(ref b, ref i, pos-1); // Left 223 | if(pos+w < b.Length) floodFill(ref b, ref i, pos+w); // Top 224 | if(pos-w >= 0) floodFill(ref b, ref i, pos-w); // Bottom 225 | } 226 | 227 | /** 228 | * Remove a part of the texture 229 | * @param {Vector2} point World point 230 | * @param {int} radius Radius of destroyed area 231 | */ 232 | public void ApplyDamage(Vector2 point, int radius) { 233 | 234 | // edit binaryImage 235 | int w = binaryImage.x, h = binaryImage.y; 236 | 237 | // get relative position of the circle 238 | Vector2 rPos = new Vector2(point.x - transform.position.x, point.y - transform.position.y); 239 | 240 | // get rotation matrix 241 | float theta = transform.rotation.eulerAngles.z * Mathf.PI / 180f; 242 | float sin = Mathf.Sin(theta); 243 | float cos = Mathf.Cos(theta); 244 | 245 | // apply rotation to the circle centre 246 | Vector2 c = new Vector2(rPos.x*cos + rPos.y*sin, -rPos.x*sin + rPos.y*cos); 247 | 248 | c.x = (xBounds + (c.x / transform.localScale.x))/(xBounds*2) * w; 249 | c.y = (yBounds + (c.y / transform.localScale.y))/(yBounds*2) * h; 250 | 251 | for (int x = 0; x < w; x++) { 252 | for (int y = 0; y < h; y++) { 253 | float dx = x-c.x; 254 | float dy = y-c.y; 255 | float dist = Mathf.Sqrt(dx*dx+dy*dy); 256 | if(dist <= radius) { 257 | binaryImage.Set(x + y*w, false); 258 | } 259 | } 260 | } 261 | 262 | updateCollider(); 263 | } 264 | 265 | // TODO: make into a coroutine?/separate thread 266 | private void updateCollider() { 267 | tex = ApplyBinaryImage2Texture(ref tex, ref binaryImage); 268 | if(doSplit) split(binaryImage); 269 | 270 | // binaryImage = tidyBinaryImage(binaryImage); 271 | BinaryImage binaryImageOutline = subtraction(binaryImage, erosion(binaryImage)); 272 | List > paths = getPaths(ref binaryImageOutline); 273 | 274 | gameObject.GetComponent().sprite = Sprite.Create(tex, gameObject.GetComponent().sprite.rect, new Vector2(0.5f, 0.5f)); 275 | 276 | setCollider(ref paths); 277 | } 278 | 279 | private void setCollider(ref List > paths) { 280 | poly.pathCount = paths.Count; 281 | islandCount = paths.Count; 282 | 283 | for(int i=0; i > getPaths(ref BinaryImage b) { 309 | int startPos; 310 | List > paths = new List >(); 311 | 312 | while(findStartPos(ref b, out startPos)) { 313 | List path = new List(); 314 | 315 | // Find path 316 | path = getPath(ref b, startPos); 317 | if(path != null) paths.Add(path); 318 | } 319 | 320 | for(int i=0; i getPath(ref BinaryImage b, int startPos) { 326 | List path = new List(); 327 | 328 | // Add start point to path 329 | path.Add(new Vector2(startPos%b.x, startPos/b.x)); 330 | 331 | int pos = 0, prevPos = startPos, currPos = startPos; 332 | bool open = true; 333 | 334 | if(!nextPos(ref b, ref pos, prevPos)) { 335 | // No other points found from the starting point means this is a single pixel island so we can remove it 336 | b.Set(currPos, false); 337 | return null; 338 | } 339 | 340 | // While there is a next pos 341 | while(open) { 342 | if(nextPos(ref b, ref pos, ref currPos, ref prevPos)) { 343 | // b.Set(pos, false); 344 | if(currPos == startPos) open = false; // We found a closed path 345 | path.Add(new Vector2(currPos%b.x, currPos/b.x)); 346 | b.Set(currPos, false); 347 | } else { 348 | // If no next position, backtrack till we find a closed path 349 | var index = backTrack(ref b, ref path, path.Count-1); 350 | 351 | if(index!=-1) { 352 | // find next new point! 353 | path.RemoveRange(index+1, path.Count-1-index); 354 | 355 | pos = (int)path[index].x + (int)path[index].y*b.x; 356 | prevPos = (int)path[index-1].x + (int)path[index-1].y*b.x; 357 | currPos = pos; 358 | 359 | } else open = false; // If we cannot, close the path (this is the worst case and will give us a buggy collider) 360 | } 361 | } 362 | 363 | return path; 364 | } 365 | 366 | private int backTrack(ref BinaryImage b, ref List path, int start) { 367 | int w = b.x; 368 | 369 | if(start <= 1) return -1; 370 | int currPos = (int)path[start].x + (int)path[start].y*w; 371 | int prevPos = (int)path[start-1].x + (int)path[start-1].y*w; 372 | 373 | if(currPos+w < b.Length && b.Get(currPos+w) && (currPos+w)!=prevPos || // Top 374 | (currPos%w)+1 < w && b.Get(currPos+1) && (currPos+1)!=prevPos || // Right 375 | currPos-w >= 0 && b.Get(currPos-w) && (currPos-w)!=prevPos || // Bottom 376 | (currPos%w)-1 >= 0 && b.Get(currPos-1) && (currPos-1)!=prevPos) { // Left 377 | return start; 378 | } 379 | else { // if we cannot find any new points set back again. 380 | start = backTrack(ref b, ref path, start-1); 381 | } 382 | 383 | return start>0?start:-1; 384 | } 385 | 386 | // Get the initial adjancent point 387 | private bool nextPos(ref BinaryImage b, ref int pos, int prevPos) { 388 | int w = b.x; 389 | 390 | if( prevPos+w < b.Length && b.Get(prevPos+w)) pos = prevPos+w; // Top 391 | else if((prevPos%w)+1 < w && b.Get(prevPos+1)) pos = prevPos+1; // Right 392 | else if(prevPos-w >= 0 && b.Get(prevPos-w)) pos = prevPos-w; // Bottom 393 | else if((prevPos%w)-1 >= 0 && b.Get(prevPos-1)) pos = prevPos-1; // Left 394 | else return false; // single pixel island 395 | 396 | return true; 397 | } 398 | 399 | // Get the adjancent point 400 | private bool nextPos(ref BinaryImage b, ref int pos, ref int currPos, ref int prevPos) { 401 | int w = b.x; 402 | 403 | if( currPos+w < b.Length && b.Get(currPos+w) && (currPos+w)!=prevPos) pos = currPos+w; // Top 404 | else if((currPos%w)+1 < w && b.Get(currPos+1) && (currPos+1)!=prevPos) pos = currPos+1; // Right 405 | else if(currPos-w >= 0 && b.Get(currPos-w) && (currPos-w)!=prevPos) pos = currPos-w; // Bottom 406 | else if((currPos%w)-1 >= 0 && b.Get(currPos-1) && (currPos-1)!=prevPos) pos = currPos-1; // Left 407 | else return false; // None 408 | 409 | // Update values 410 | prevPos = currPos; 411 | currPos = pos; 412 | 413 | return true; 414 | } 415 | 416 | // first stage of similification 417 | // Determine if we need to add this point(vertex) to the path 418 | private List simplify(ref BinaryImage b, List path) { 419 | List t = new List(); 420 | 421 | t.Add(path[0]); 422 | 423 | // remove straight line vertices 424 | for(int i=1; i p = new List(); 440 | p.Add(t[0]); 441 | for(int i=1; i 1) p.Add(t[i]); // TODO: find better constant / average weight? W = (w0+..+wk) / k 443 | } 444 | p.Add(t[t.Count-1]); 445 | 446 | return p; 447 | } 448 | 449 | /** 450 | * Subtracts one binaryImage from another 451 | * @param {BinaryImage} b1 [description] 452 | * @param {BinaryImage} b2 [description] 453 | * @return {BinaryImage} [description] 454 | */ 455 | private BinaryImage subtraction(BinaryImage b1, BinaryImage b2) { 456 | BinaryImage t = new BinaryImage(b1.x, b1.y); 457 | 458 | int w = b1.x; // width 459 | int h = b1.y; // height 460 | 461 | for(int x=0; x=0 && j=0) { 487 | if(!b.Get(i + j*w)) t.Set(x + y*w, false); 488 | } 489 | else t.Set(x + y*w, false); 490 | } 491 | } 492 | } 493 | return t; 494 | } 495 | 496 | /** 497 | * If the centre of a 3x3 is true then make the whole grid true 498 | * @param {BinaryImage} ref BinaryImage b [description] 499 | * @return {BinaryImage} The dilated image 500 | */ 501 | private BinaryImage dilation(BinaryImage b) { 502 | int[,] dirs = {{0,1},{1,1},{1,0},{1,-1},{0,-1},{-1,-1},{-1,0},{-1,1}}; 503 | BinaryImage t = new BinaryImage(b.x, b.y); 504 | 505 | int w = b.x; // width 506 | int h = b.y; // height 507 | 508 | for(int x=0; x=0 && j=0) 514 | t.Set(i + j*w, true); 515 | } 516 | } 517 | } 518 | } 519 | return t; 520 | } 521 | 522 | /** 523 | * Generates a BitArray from a Texture2D 524 | * @param {Texture2D} t [] 525 | * @return {BitArray} [description] 526 | */ 527 | private BinaryImage BinaryImageFromTex(ref Texture2D t) { 528 | BinaryImage b = new BinaryImage(t.width, t.height); 529 | 530 | Color[] data = t.GetPixels(); 531 | 532 | for(int x=0; x 0 ); 533 | 534 | return b; 535 | } 536 | 537 | private Texture2D ApplyBinaryImage2Texture(ref Texture2D tex, ref BinaryImage b) { 538 | Texture2D t = new Texture2D(b.x, b.y); 539 | t.wrapMode = TextureWrapMode.Clamp; 540 | 541 | Color[] data = tex.GetPixels(); 542 | 543 | for(int x=0; x > paths) { 572 | List colorList = new List() { 573 | Color.red, 574 | Color.green, 575 | Color.blue, 576 | Color.magenta, 577 | Color.yellow 578 | }; 579 | 580 | Texture2D t = new Texture2D(b.x, b.y); 581 | t.wrapMode = TextureWrapMode.Clamp; 582 | 583 | for(int i=0; i()) 20 | colliders[i].GetComponent().ApplyDamage(explosionPos, explosionRadius); 21 | } 22 | } 23 | 24 | public void FixedUpdate() 25 | { 26 | GetComponent().AddForce(transform.up*-1); 27 | } 28 | 29 | void OnTriggerEnter2D (Collider2D other) 30 | { 31 | if (dead) { 32 | return; 33 | } 34 | 35 | GameObject go = other.gameObject; 36 | if (go != null && go.layer == LayerMask.NameToLayer("Level")) 37 | { 38 | Destroy(gameObject); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Destructible sprites with polygon colliders 2 | 3 | 4 | ## Usage 5 | Attach `DestructibleSprite.cs` as a script component to the sprite. 6 | 7 | When a projectile hits the sprites polygon collider call `ApplyDamage(pos, radius)` at the hit point. 8 | 9 | `colliders[i].GetComponent().ApplyDamage(explosionPos, explosionRadius)` 10 | 11 | ## Generating a polygon collider for a sprite at runtime 12 | 13 | 14 | We start by generating a binary image (b) from the texture. 15 | 16 | We can use two helpful functions on this binary image to get a nice clean outline. 17 | 18 | 19 | 20 | Erosion (E) - shrinking of the image 21 | 22 | 23 | 24 | Dilation (D) - boarding of image 25 | 26 | We can clean up our binary image by appply these functions. 27 | b0= D(E(b)) 28 | 29 | We can get the outline of the image bs = b0 - E(b0) 30 | 31 | 32 | 33 | Subtraction (S) 34 | 35 | We can add each pixel now as a vertex to the polygon collider. Performing some simplification we get a simple path the collider can use. 36 | 37 | 38 | 39 | 40 | Each point is a vertex on the collider. 41 | 42 | 43 | 44 | ## License 45 | 46 | [BSD license](http://opensource.org/licenses/bsd-license.php) 47 | -------------------------------------------------------------------------------- /ShootProjectile.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Collections; 3 | 4 | public class ShootProjectile : MonoBehaviour { 5 | 6 | public Transform spawnPosition; 7 | public GameObject projectilePrefab; 8 | public float force = 10f; 9 | 10 | void Update () { 11 | if(Input.GetMouseButtonDown(0)) { 12 | Vector3 sp = Camera.main.WorldToScreenPoint(spawnPosition.position); 13 | Vector3 dir = (Input.mousePosition - sp).normalized; 14 | 15 | GameObject projectile = GameObject.Instantiate(projectilePrefab, spawnPosition.position, Quaternion.identity) as GameObject; 16 | projectile.GetComponent().velocity += (Vector2)dir*force; 17 | 18 | } 19 | } 20 | } 21 | --------------------------------------------------------------------------------