Worms Style Destructable Terrain Help
Monkey Forums/Monkey Programming/Worms Style Destructable Terrain Help
| ||
Was going to test Worms-Style Destructable Terrain, adding it as a Monkey example. Found this: http://web.archive.org/web/20090101215451/http://blog.xna3.com/2007/12/2d-deformable-level.html I'm not just getting it to work properly, can you solve the mystery? [monkeycode] 'create a new image...single frame only? 'Function CreateImage:Image( width,height,flags ) 'read from 'backbuffer'... 'Function ReadPixels:Void( pixels:Int[],x:Int,y:Int,width:Int,height:Int,arrayOffset:Int=0,arrayPitch:Int=0 ) 'write to image...must be a CreateImage image. 'Method Image.WritePixels( pixels:Int[],x:Int,y:Int,width:Int,height:Int,arrayOffset:Int=0,arrayPitch:Int=0 ) Import mojo Function Main:Int() New MyApp() Return 0 End Global ToolWidth:Int = 64 Global ToolHeight:Int = 64 Global MapWidth:Int = 800 Global MapHeight:Int = 600 Class MyApp Extends App Field sky:Image Field ground:Image Field tool:Image Field destructableGround:Image 'Above: This image is created by CreateImage and will be a editable copy of ground Field groundPixels:Int[] = [MapWidth*MapHeight] 'Above: Every single pixel of the ground.png image will be put in this array Field tool1Pixels:Int[] = [ToolWidth*ToolHeight] Field tool1Mask:Int[] = [ToolWidth*ToolHeight] 'Above: Every single pixel of the tool.png image will be put in this array Field FirstFrame:Bool = True 'Above: A simple hack to run the InitializeTerrain Method only once Method OnCreate() SetUpdateRate(60) sky = LoadImage("sky.png") ground = LoadImage("ground.png") tool = LoadImage("tool.png") destructableGround = CreateImage(MapWidth,MapHeight) End Method OnUpdate() End 'ReadPixels Buffer,0,0,40,40 'TargetImage.WritePixels Buffer,0,0,80,40 Method InitializeTerrain() DrawImage(ground,0,0) ' From Docs: ' ReadPixels( pixels:Int[], x:Int, y:Int, width:Int, height:Int, arrayOffset:Int=0, arrayPitch:Int=0 ) ReadPixels( groundPixels,0,0,MapWidth,MapHeight ) DrawImage(tool,0,0) ReadPixels( tool1Pixels,0,0,ToolWidth,ToolHeight ) ReadPixels( tool1Mask,0,0,ToolWidth,ToolHeight ) ' For Local i = 0 To ToolWidth*ToolHeight-1 ' If tool1Pixels[i] > 0 ' tool1Mask[i] = 0 ' Else ' tool1Mask[i] = 1 '' End ' Next 'Write can only be done on an Image created by CreateImage() destructableGround.WritePixels( groundPixels,0,0,MapWidth,MapHeight ) destructableGround.WritePixels( tool1Pixels,0,0,ToolWidth,ToolHeight) End ' This Adds terrain Method AddTerrain() ReadPixels( tool1Pixels,MouseX-ToolWidth/2,MouseY-ToolHeight/2,ToolWidth,ToolHeight ) destructableGround.WritePixels( tool1Pixels,MouseX-ToolWidth/2,MouseY-ToolHeight/2,ToolWidth,ToolHeight ) End Method RemoveTerrain() Local counter:Int = 0 ReadPixels( tool1Pixels,MouseX-ToolWidth/2,MouseY-ToolHeight/2,ToolWidth,ToolHeight ) For Local x = 0 until ToolWidth For Local y = 0 until ToolHeight 'Print "Pixel [x,y] = ["+x+","+y+"] = "+tool1Pixels[x*y] If tool1Mask[x * y] = -16777216 tool1Pixels[x*y] = groundPixels[(Int(MouseX) + x) + (Int(MouseY) + y)];'' counter += 1 Else tool1Pixels[x*y] = 0 End Next Print tool1Mask[x*32] Next Print "Found "+counter+" pixels with Alpha" destructableGround.WritePixels( tool1Mask,MouseX-ToolWidth/2,MouseY-ToolHeight/2,ToolWidth,ToolHeight ) End Method OnRender() If FirstFrame Then InitializeTerrain() FirstFrame = False Cls(0, 0, 0) DrawImage(sky,0,0) DrawImage(destructableGround,0,0) SetColor( 0,153,0) DrawCircle(MouseX,MouseY,ToolHeight/2) 'Read & Write SetColor( 255,255,255) 'DrawImage(tool,MouseX,MouseY) If MouseDown(MOUSE_LEFT) And KeyDown(KEY_CONTROL) = False If EditMode1 = False Then AddTerrain EditMode1 = True Else EditMode1 = False End If ( MouseDown(MOUSE_LEFT) And KeyDown(KEY_CONTROL) ) If EditMode2 = False Then RemoveTerrain EditMode2 = True Else EditMode2 = False End End Field EditMode1:Bool = False Field EditMode2:Bool = False End [/monkeycode] Here is a downloadable with graphics included. www.truplo.com/UploadedFiles/destructableTerrain.zip |
| ||
I think you are nearly there (although the code is slightly messy). With your tool.png just create an empty image (ie one that is only alpha), does that solve your issue? (You havent actually said what the issue is). To make this playable in-game, I think you would have to make your Terrain into a grid and only alter that grids pixels. |
| ||
Hi Tibit, I wrote a quite example to show how this can be done. You will need the deform sprite from the link you posted, I also used their ground image as well so I would grab that: http://web.archive.org/web/20110609003109/http://sites.google.com/site/edwarddennekamp/deform.png It would definitely benefit from therevills suggestion of using a grid. Anyway heres the code example: [monkeycode] Strict Import mojo Global tool : Image Global toolPixels : Int[] Global ground : Image Global groundPixels : Int[] Global loaded : Bool = False Class MyApp Extends App Method OnCreate : int() SetUpdateRate( 60 ) ground = LoadImage( "level.png" ) tool = LoadImage( "deform.png" ) Return 0 End Method Method OnUpdate : Int() If MouseHit() Local w : Int = ground.Width() Local h : Int = ground.Height() Local tw : Int = tool.Width() Local th : Int = tool.Height() '// offset mouse by tool size ( center tool ) Local mx : Int = MouseX() - ( tw / 2.0 ) Local my : Int = MouseY() - ( th / 2.0 ) '// calculate starting pixel index Local ti : Int = w * my + mx '// loop through tool pixels For Local y : Int = 0 Until th For Local x : Int = 0 Until tw '// if current location is on the screen If mx + x < DeviceWidth() And my + y < DeviceHeight() And mx + x > -1 And my + y > -1 Local argb : Int '// get ground alpha argb = groundPixels[ ti + x ] Local ga : Int = ( argb Shr 24 ) & $ff '// get tool alpha and RGB to check for white delete part argb = toolPixels[( y * tw ) + x ] Local ta : Int = ( argb Shr 24 ) & $ff Local tr : Int = ( argb Shr 16 ) & $ff Local tg : Int = ( argb Shr 8 ) & $ff Local tb : Int = argb & $ff '// if neither are transparent write the tool to the ground pixels (draws white tool section on the ground) If ga <> 0 And ta <> 0 'if current pixel of tool is white we want to cut this part out so use alpha color instead If tr = 255 And tg = 255 And tb = 255 groundPixels[ ti + x ] = 0 'if current pixel of tool is not white, e.g. black border, leave this as it is Else groundPixels[ ti + x ] = toolPixels[( y * tw ) + x ] Endif Endif endif Next '// increment pixel index by the ground width to get the next rows starting index ti = ti + w Next '// update ground image ground.WritePixels( groundPixels, 0, 0, w, h ) Endif Return 0 End Method Method OnRender : Int() '// Creates Manipulatable Images If loaded = False CreateCustomImages() loaded = True Endif Cls 64, 96, 160 '// Draws Images If ground <> Null Then DrawImage( ground, 0, 0 ) If tool <> Null Then DrawImage( tool, MouseX(), MouseY() ) Return 0 End Method End Class Function Main : Int() New MyApp() Return 0 End Function Function CreateCustomImages : Void() '// Create space for tool Local w : Int = tool.Width() Local h : Int = tool.Height() Local pixels : Int[ w * h ] Local img : Image Cls 128, 0, 255 '// Grab tool sprite and mask background colour DrawImage tool, 0, 0 ReadPixels( pixels, 0, 0, w, h ) PixelArrayMask( pixels, 128, 0, 255 ) img = CreateImage( w, h, 1, Image.MidHandle ) img.WritePixels( pixels, 0, 0, w, h ) tool = img toolPixels = pixels '// Create space for ground w = Min( ground.Width(), DeviceWidth() ) h = Min( ground.Height(), DeviceHeight() ) pixels = New Int[ w * h ] Cls 128, 0, 255 '// Grab ground sprite and mask background colour DrawImage ground, 0, 0 ReadPixels( pixels, 0, 0, w, h ) PixelArrayMask( pixels, 128, 0, 255 ) img = CreateImage( w, h ) img.WritePixels( pixels, 0, 0, w, h ) ground = img groundPixels = pixels End Function '// Converts Mask Pixel Color to Transparent Pixel Function PixelArrayMask : void( pixels : Int[], mask_r : Int = 0, mask_g : Int = 0, mask_b : Int = 0 ) For Local i : Int = 0 Until pixels.Length Local argb : Int = pixels[ i ] Local a : Int = ( argb Shr 24 ) & $ff Local r : Int = ( argb Shr 16 ) & $ff Local g : Int = ( argb Shr 8 ) & $ff Local b : Int = argb & $ff If a = 255 And r = mask_r And g = mask_g And b = mask_b a = 0 argb = ( a Shl 24 ) | ( r Shl 16 ) | ( g Shl 8 ) | b pixels[ i ] = argb Endif Next End Function [/monkeycode] |
| ||
I have been looking at this, but I just can't get it to work. Noodle, your example first says Write cannot be performed in OnUpdate, I fixed that, but then nothing happens at all at MouseHit. I used the correct images for your sample. Can you or someone else get it to run properly? It feels like there is some minor thing missing? I tested on html5 and glhw targets. The aim: * I want to be able to add a shape to the terrain [This works] * I want to be able to remove a shape from the terrain [This works ONLY for a rectangular shape] So the intention with my code above and Noodle's Mask code is to use an image as the tool and use that image's white (or non alpha) pixels (as an example) to add alpha (that is remove pixels) from the background image. And yes I agree that the right tactic might be using a grid and rendering tiles of images. However this technique was done back in Worms in 1995, it just blows my mind it cannot be done with hardware from 2012 Hah ;) |
| ||
This works:Strict Import mojo Global tool : Image Global toolPixels : Int[] Global ground : Image Global groundPixels : Int[] Global loaded : Bool = False Global pointIndexsToKeep:IntList = New IntList Class MyApp Extends App Method OnCreate : int() SetUpdateRate( 60 ) ground = LoadImage( "level.png" ) tool = LoadImage( "deform.png" ) Return 0 End Method Method OnUpdate : Int() If MouseHit() Local w : Int = ground.Width() Local h : Int = ground.Height() Local tw : Int = tool.Width() Local th : Int = tool.Height() '// offset mouse by tool size ( center tool ) Local mx : Int = MouseX() - ( tw / 2.0 ) Local my : Int = MouseY() - ( th / 2.0 ) '// calculate starting pixel index Local ti : Int = w * my + mx '// loop through tool pixels For Local y : Int = 0 Until th For Local x : Int = 0 Until tw '// if current location is on the screen If mx + x < DeviceWidth() And my + y < DeviceHeight() And mx + x > -1 And my + y > -1 Local argb : Int '// get ground alpha argb = groundPixels[ ti + x ] Local ga : Int = ( argb Shr 24 ) & $ff '// get tool alpha and RGB to check for white delete part argb = toolPixels[( y * tw ) + x ] Local ta : Int = ( argb Shr 24 ) & $ff Local tr : Int = ( argb Shr 16 ) & $ff Local tg : Int = ( argb Shr 8 ) & $ff Local tb : Int = argb & $ff '// if neither are transparent write the tool to the ground pixels (draws white tool section on the ground) If ga <> 0 And ta <> 0 'if current pixel of tool is white we want to cut this part out so use alpha color instead If tr = 255 And tg = 255 And tb = 255 groundPixels[ ti + x ] = 0 'if current pixel of tool is not white, e.g. black border, leave this as it is Else groundPixels[ ti + x ] = toolPixels[( y * tw ) + x ] Endif Endif endif Next '// increment pixel index by the ground width to get the next rows starting index ti = ti + w Next Endif Return 0 End Method Method OnRender : Int() '// Creates Manipulatable Images If loaded = False CreateCustomImages() loaded = True Endif Cls 64, 96, 160 Local w : Int = ground.Width() Local h : Int = ground.Height() '// update ground image ground.WritePixels( groundPixels, 0, 0, w, h ) '// Draws Images If ground <> Null Then DrawImage( ground, 0, 0 ) If tool <> Null Then DrawImage( tool, MouseX(), MouseY() ) Return 0 End Method End Class Function Main : Int() New MyApp() Return 0 End Function Function CreateCustomImages : Void() '// Create space for tool Local w : Int = tool.Width() Local h : Int = tool.Height() Local pixels : Int[ w * h ] Local img : Image Cls 128, 0, 255 '// Grab tool sprite and mask background colour DrawImage tool, 0, 0 ReadPixels( pixels, 0, 0, w, h ) PixelArrayMask( pixels, 128, 0, 255 ) img = CreateImage( w, h, 1, Image.MidHandle ) img.WritePixels( pixels, 0, 0, w, h ) tool = img toolPixels = pixels '// Create space for ground w = Min( ground.Width(), DeviceWidth() ) h = Min( ground.Height(), DeviceHeight() ) pixels = New Int[ w * h ] Cls 128, 0, 255 '// Grab ground sprite and mask background colour DrawImage ground, 0, 0 ReadPixels( pixels, 0, 0, w, h ) PixelArrayMask( pixels, 128, 0, 255 ) img = CreateImage( w, h ) img.WritePixels( pixels, 0, 0, w, h ) ground = img groundPixels = pixels End Function '// Converts Mask Pixel Color to Transparent Pixel Function PixelArrayMask : void( pixels : Int[], mask_r : Int = 0, mask_g : Int = 0, mask_b : Int = 0 ) For Local i : Int = 0 Until pixels.Length Local argb : Int = pixels[ i ] Local a : Int = ( argb Shr 24 ) & $ff Local r : Int = ( argb Shr 16 ) & $ff Local g : Int = ( argb Shr 8 ) & $ff Local b : Int = argb & $ff If a = 255 And r = mask_r And g = mask_g And b = mask_b a = 0 argb = ( a Shl 24 ) | ( r Shl 16 ) | ( g Shl 8 ) | b pixels[ i ] = argb Endif Next End Function |
| ||
Noodle, your example first says Write cannot be performed in OnUpdate, This is a bug in Monkey, you need to run it in Release mode: http://www.monkeycoder.co.nz/Community/posts.php?topic=3492 |
| ||
This is a bug in Monkey, you need to run it in Release mode Yea the code I posted runs fine. I would recommend using that instead of CopperCircle's edit as that writes to the ground image every render which will be slow. This only needs to be done when the ground image is modified. Hopefully this incorrect error message will be fixed in the next release. |
| ||
Awesome, it works now! Thanks :) |
| ||
If I were doing this, and I may well be doing something similar, I would store the terrain data in a space partitioning tree and render it as sub-rects out of a tilemap. If the tile is undamaged, draw the full tile. If it is totally destroyed, don't draw anything. If it is partially damaged, go into each quadrant and perform the same operations. This should be much faster to draw as well as being far more memory efficient. What I would really like would be a way to tell if a piece of terrain was disconnected from the rest of the terrain-- a stalactite shot off the ceiling or an overhang blown off. Then that piece would become a physics object of mass equal to its number of pixels, and fall. It seems like you could do this with some sort of floodfill algorithm and it might be possible to speed it up immensely with the bsp tree, somehow. These checks would also only need to happen when terrain was actually destroyed and it might be possible to amortize it out across a number of frames. Or you could see if an explosion connected empty space to other empty space. |
| ||
Yea Therevills suggested something similar. All you would really need is to divide the level up into a grid of individual modifiable images with pixel arrays. Check for intersection with AABB (pixel AABB not including alpha pixels) and modify the overlapping grid rects. That should be fast enough for realtime use, I was planning to test this out at some point but busy working on my own projects. |