Worms Style Destructable Terrain Help

Monkey Forums/Monkey Programming/Worms Style Destructable Terrain Help

Tibit(Posted 2012) [#1]
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


therevills(Posted 2012) [#2]
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.


NoOdle(Posted 2012) [#3]
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]


Tibit(Posted 2012) [#4]
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 ;)


CopperCircle(Posted 2012) [#5]
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



therevills(Posted 2012) [#6]
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


NoOdle(Posted 2012) [#7]
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.


Tibit(Posted 2012) [#8]
Awesome, it works now!

Thanks :)


zoqfotpik(Posted 2012) [#9]
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.


NoOdle(Posted 2012) [#10]
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.