functions/utility/New-MiniGameCollisionUtility.ps1
|
function New-MiniGameCollisionUtility { <# .SYNOPSIS This return a utility class for detecting collisions. .DESCRIPTION This return a utility class for detecting collisions. Use the New-MiniGameRunUtility or New-MiniGameObjectUtility for effective use. .OUTPUTS .EXAMPLE PS> (Use with New-MiniGameObjetUtility) - Add two movable blocks with radius collision method: $object = New-MiniGameObjectUtility -CollisionMethod Radius $block1 = $object.Add('Block 1') $block1.Rightmost() $block1.Move(-23,5) $block1.CollisionGroup() $block1.AddCanvas(@("###"," # ","###")) $block2 = $object.Add('Block 2') $block2.Rightmost() $block2.Move(-19,5) $block2.CollisionGroup() $block2.AddCanvas(@("###"," # ","###")) clear $block1.Redraw() $block2.Redraw() $script = { param($self, $other) Write-Host "`nI'm $($self.Id) colliding with $($other.Id)" } $block1.OnCollision($script) $block2.OnCollision($script) Start-Sleep -Seconds 2 $block2.Move(-1, 0) $block2.Redraw() $block1.Redraw() $block1.Collisions() Start-Sleep -Seconds 2 $block2.Move(-1, 0) $block2.Redraw() $block1.Redraw() $block1.Collisions() Start-Sleep -Seconds 2 $block2.Move(-1, 0) $block2.Redraw() $block1.Redraw() $block2.Collisions() Start-Sleep -Seconds 2 $object.Collisions() Start-Sleep -Seconds 2 #### #### #### (Standalone Custom Use) NOTE: There is no drawing on Canvas, this only calculates positions. Use via the New-MiniGameRunUtility or New-MiniGameObjectUtility for drawing. PS> $collision = New-MiniGameCollisionUtility -Method Position $box1 = $collision.Add('Box 1') $box1.SetPos(0, 5) $box1.SetCanvas(@( '###', ' # ', '###' )) $box1.OnCollision({ param($self, $colliders) Write-Host "`nI'm $($self.id) colliding with $($colliders.id)" }) $box2 = $collision.Add('Box 2') $box2.SetPos(2, 5) $box2.SetCanvas(@( ' ##', '###', ' ##' )) # Should not collide with Box 2, since they are not overlapping. $box1.Collisions() Start-Sleep -Seconds 2 # After moving one it should now collide. $box2.SetPos(1, 5) $box1.Collisions() .LINK #> [CmdletBinding()] param( [Parameter( Mandatory = $true )] [ValidateSet( 'Position', 'Radius' )] [System.String] $Method ) class MiniGameCollisionUtilityObjectBase { [System.String] $_group [System.Object] $_manager [System.Object] $ref [System.String] $id [System.Boolean] $_valid [System.String[]] $canvas [System.Numerics.Vector2] $pos [System.Collections.Hashtable] $_onCollision = @{} [System.Collections.Hashtable] $_currentCollisions = @{} # Set a reference object to provide in the OnCollisions-Function, instead of this object. [void] SetRef([System.Object] $ref) { $this.ref = $ref } [void] SetPos([System.Numerics.Vector2] $pos) { $this.SetPos($pos.X, $pos.Y) } [void] SetPos([Int32] $x, [Int32] $y) { $this.pos = [System.Numerics.Vector2]@{X = $x; Y = $y } $this._valid = $false $this._manager._processed[$this._group] = $false } [void] SetCanvas([System.String[]] $canvas) { $this.canvas = $canvas $this._valid = $false $this._manager._processed[$this._group] = $false } [void] OnCollision([scriptblock] $action) { $this._onCollision.Remove($action.Id.Guid) $this._onCollision.Add($action.Id.Guid, $action) } [void] InvokeCollisions() { if ($this._currentCollisions.Count -EQ 0) { return } foreach ($action in $this._onCollision.Values) { if ($null -NE $this.ref) { $action.Invoke($this.ref, $this._currentCollisions.Values) } else { $action.Invoke($this, $this._currentCollisions.Values) } } } [void] Collisions() { $this._manager.Calculate($this._group) $this.InvokeCollisions() } } class MiniGameCollisionUtilityBase { [System.Collections.Hashtable] $_groups = @{} [System.Collections.Hashtable] $_processed = @{} [MiniGameCollisionUtilityObjectBase] Add( [MiniGameCollisionUtilityObjectBase] $object, [System.String] $group ) { if (-NOT $this._groups.ContainsKey($group)) { $this._groups.Add($group, @{}) $this._processed.Add($group, $false) } $this._groups[$group].Add($object.id, $object) $object._manager = $this $object._group = $group return $object } [MiniGameCollisionUtilityObjectBase] Add( [MiniGameCollisionUtilityObjectBase] $object ) { return $this.Add($object, '__default__') } [void] Collisions([System.String] $group) { $this.Calculate($group) # Invoke the collision handler on each object. foreach ($collisionObject in $this._groups[$group].Values) { $collisionObject.InvokeCollisions() } } [void] Collisions() { $this.Collisions('__default__') } } <# Find collisions based on a radius around the center #> class MiniGameCollisionUtilityObjectRadiusMethod : MiniGameCollisionUtilityObjectBase { [System.Single] $_radius = -1 [System.Numerics.Vector2] $_center [void] SetRadius([System.Single] $radius) { $this._radius = $radius $this._valid = $false } [void] _process() { if ($this._valid) { return } $this._valid = $true $rowCount = $this.canvas.Count $rowMiddle = [System.Math]::Floor($rowCount / 2) $colCount = $this.canvas[$rowMiddle].Length $colMiddle = [System.Math]::Floor($colCount / 2) $this._center = [System.Numerics.Vector2]@{ X = $this.pos.X + $colMiddle Y = $this.pos.Y + $rowMiddle } if (-1 -EQ $this._radius) { $this._radius = (($rowCount / 2) + ($colCount / 2)) / 2 } } } class MiniGameCollisionUtilityRadiusMethod : MiniGameCollisionUtilityBase { [MiniGameCollisionUtilityObjectRadiusMethod] Add( [System.String] $id, [System.String] $group ) { return ([MiniGameCollisionUtilityBase]$this).Add( [MiniGameCollisionUtilityObjectRadiusMethod]@{_manager = $this; id = $id }, $group ) } [MiniGameCollisionUtilityObjectRadiusMethod] Add([System.String] $id) { return ([MiniGameCollisionUtilityBase]$this).Add( [MiniGameCollisionUtilityObjectRadiusMethod]@{_manager = $this; id = $id } ) } [void] Calculate([System.String] $group) { # If for the current state everything has been processed, don't calculate collisions again. # This will be invalidated, as soon as a property on a collision object changes. if ($this._processed[$group]) { return } else { $this._processed[$group] = $true } $collisonGroup = $this._groups[$group] foreach ($object in $collisonGroup.Values) { $object._currentCollisions = @{} $object._process() } foreach ($main in $collisonGroup.Values) { foreach ($other in $collisonGroup.Values) { if ($main -EQ $other) { continue } if ($main._currentCollisions.ContainsKey($other.id)) { continue } $dist = [System.Numerics.Vector2]@{ X = $main._center.X - $other._center.X Y = $main._center.Y - $other._center.Y } $dist = $dist.X * $dist.X + $dist.Y * $dist.Y $rad = $main._radius * $main._radius + $other._radius * $other._radius if ($dist -LT $rad) { $main._currentCollisions.Add($other.id, $other) $other._currentCollisions.Add($main.id, $main) } } } } } <# Find collisions based on exact positions. (This doesn't work if the positions aren't integer values!) #> class MiniGameCollisionUtilityObjectPositionMethod : MiniGameCollisionUtilityObjectBase { [System.Numerics.Vector2[]] $_occupied [System.Numerics.Vector2[]] _process() { if ($this._valid) { return $this._occupied } $this._valid = $true $this._occupied = @() for ($row = 0; $row -LT $this.canvas.Count; $row++) { for ($col = 0; $col -LT $this.canvas[$row].Length; $col++) { $char = $this.canvas[$row][$col] if ([System.String]::IsNullOrWhiteSpace($char)) { continue } $this._occupied += [System.Numerics.Vector2]@{ X = [System.Math]::round($this.pos.X + $col) Y = [System.Math]::round($this.pos.Y + $row) } } } return $this._occupied } } class MiniGameCollisionUtilityPositionMethod : MiniGameCollisionUtilityBase { [MiniGameCollisionUtilityObjectPositionMethod] Add( [System.String] $id, [System.String] $group ) { return ([MiniGameCollisionUtilityBase]$this).Add( [MiniGameCollisionUtilityObjectPositionMethod]@{_manager = $this; id = $id }, $group ) } [MiniGameCollisionUtilityObjectPositionMethod] Add([System.String] $id) { return ([MiniGameCollisionUtilityBase]$this).Add( [MiniGameCollisionUtilityObjectPositionMethod]@{_manager = $this; id = $id } ) } [void] Calculate([System.String] $group) { # If for the current state everything has been processed, don't calculate collisions again. # This will be invalidated, as soon as a property on a collision object changes. if ($this._processed[$group]) { return } else { $this._processed[$group] = $true } [System.Collections.Hashtable] $dections = @{} [System.Collections.Hashtable] $temp = @{} $collisonGroup = $this._groups[$group] foreach ($object in $collisonGroup.Values) { $object._currentCollisions = @{} foreach ($pos in $object._process()) { # If two or more object occupy the same space, # then they are colliding with each other if ($temp.ContainsKey($pos)) { $dections.Remove($pos) $dections.Add($pos, $pos) $temp[$pos] += $object } else { $temp.Add($pos, @($object)) } } } # Those are all postion on which a collision occured. # However multiple position can belong to the same object. foreach ($pos in $dections.Values) { # Temp has all the objects at the same position. # Multiple objects on the same position collide with each other. $objects = $temp[$pos] foreach ($main in $objects) { foreach ($other in $objects) { # Don't add a collision to itself! if ($main -EQ $other) { continue } # Collect all colliding objects in the collision object. if (-NOT $main._currentCollisions.ContainsKey($other.id)) { if ($null -NE $other.ref) { $main._currentCollisions.Add($other.id, $other.ref) } else { $main._currentCollisions.Add($other.id, $other) } } } } } } } if ($Method -IEQ 'Radius') { return [MiniGameCollisionUtilityRadiusMethod]@{} } elseif ($Method -IEQ 'Position') { return [MiniGameCollisionUtilityPositionMethod]@{} } } |