functions/utility/New-MiniGameObjectUtility.ps1
|
function New-MiniGameObjectUtility { <# .SYNOPSIS This return a utility class for managing gameobjects. - Use New-MiniGame for creating a game loop. - The object utility is used to create and manage objects in the game. .DESCRIPTION This return a utility class for managing gameobjects. An object: - has a position and optional velocity - supports Draw, Redraw, Undraw, Remove and Move - can have multple canvases (sides of a character) and switch between Can be used by itself or via New-MiniGame .OUTPUTS .EXAMPLE Creating two stickmans and moving the left one towards the right stickman: PS> $object = New-MiniGameObjectUtility $blueprint = $object.AddBlueprint('Stickman') $blueprint.Rightmost() $blueprint.Foreground.SetColor('White') $blueprint.AddCanvas( 'normal', @( " Ö ", " /|\ ", " | ", " / \ " ) ) $blueprint.AddCanvas( 'happy', @( " \ Ö / ", " | ", " | ", " / \ " ) ) $blueprint.CollisionGroup() $blueprint.OnCollision({ param($self, $colliders) Write-Host "`nI'm $($self.id) colliding with $($colliders[0].id)" }) $stickman1 = $object.FromBlueprint($blueprint, 'Stickman 1') $stickman1.Move(-23, 5) $stickman2 = $object.FromBlueprint('Stickman', 'Stickman 2') $stickman2.Move(-17, 5) $child = $stickman2.AddChild('Stickman Child') # Moves relative to the parent position $child.Move(3, 0) $child.Foreground.SetColor('Red') $child.AddCanvas(@( " Ö ", " ´|`` " )) $stickman2 $stickman1 clear $stickman1.Redraw() $stickman2.Redraw() $stickman2.Collisions() $stickman1.Collisions() Start-Sleep -Seconds 2 $stickman2.setCanvas('happy') $stickman2.Redraw() Start-Sleep -Seconds 2 $stickman2.Move(-1, 0) $stickman2.Redraw() Start-Sleep -Seconds 2 $stickman2.setCanvas('normal') $stickman2.Move(-1, 0) $stickman2.Redraw() Start-Sleep -Seconds 2 $stickman2.Move(-2, 0) $stickman2.Redraw() $stickman2.Collisions() $stickman1.Collisions() Start-Sleep -Seconds 2 $stickman1.Remove() Start-Sleep -Seconds 2 $stickman2.Remove() Start-Sleep -Seconds 2 .LINK #> [CmdletBinding()] param ( [Parameter( Position = 0, Mandatory = $false )] [System.Int32] $MaximumX = -1, [Parameter( Position = 1, Mandatory = $false )] [System.Int32] $MaximumY = -1, [Parameter( Position = 2, Mandatory = $false )] [System.Int32] $MinimumX = 0, [Parameter( Position = 3, Mandatory = $false )] [System.Int32] $MinimumY = 0, # Inverts drawing on the Y-Axis. Normally Y=0 points to the top of the screen. # This is usefull for having elements fly from the bottom upwards. [Parameter()] [switch] $InvertY, # Inverts drawing on the X-Axis. Normally X=0 points to the left side of the screen. # Might not be useful but complement to InvertY [Parameter()] [switch] $InvertX, <# The method of calculating positions. - Radius: Draws a radius around each object and calculates its distance (more performant with many objects) - Position: Compares all occupied positions of each object Use radius if Position causes performance issues. #> [Parameter()] [ValidateSet('Position', 'Radius')] [System.String] $CollisionMethod = 'Position', # This automatically trims empty spaces from inputet strings. [Parameter( Mandatory = $false )] [System.Boolean] $TrimSpaces = $true ) <# /////////////////////////////////////////////////////////////////////// Every object has this for foreground and background. It gives color information when calling the draw utility. #> class MiniGameObjectUtilityObjectGraphics { [System.String] $_color = $null [System.Byte[]]$_bytes = $null [void] Reset() { $this._color = $null $this._bytes = $null } # There are some not so nice code here like this. # It receives the MiniGameDrawUtilityGraphics from the draw utility, # to set the color information for the next draw method call. # It should only be called internally. [void] _draw([System.Object] $drawUtilityGraphics) { if (-NOT [System.String]::IsNullOrEmpty($this._color)) { $drawUtilityGraphics.OnceColor($this._color) } elseif ($null -NE $this._bytes -AND $this._bytes.Count -EQ 1) { $drawUtilityGraphics.Once8Bit($this._bytes[0]) } elseif ($null -NE $this._bytes -AND $this._bytes.Count -EQ 3) { $drawUtilityGraphics.OnceRGB($this._bytes) } } [void] setColor([System.String] $color) { $this.Reset() $this._color = $color } [void] Set8Bit([System.Byte] $byte) { $this.Reset() $this._byte = $byte } [void] SetRGB( [System.Byte] $red, [System.Byte] $green, [System.Byte] $blue ) { $this.Reset() $this._bytes = @($red, $green, $blue) } [void] SetRGB([System.Byte[]] $rgb) { $this.SetRGB($rgb[0], $rgb[1], $rgb[2]) } } <# /////////////////////////////////////////////////////////////////////// This is an object that can be drawn on the screen. - Blueprint: This is a template for creating many similar objects. - Group: This is to easily group objects together. like 'obstacles', 'stars', etc. - Tag: Additional tags for objects. like 'enemy', 'player', etc. TODO: Method to retrieve objects by tag. - Variables: Variables for each object. like 'health', 'score', etc. - Position: The position of the object on the screen. - Velocity: (Optional) The velocity of the object. This is used for moving the object. - Offset: Automatically set on childs and used for moving the object relative to its parent. #> class MiniGameObjectUtilityObject { static [MiniGameObjectUtilityObject] FromBlueprint( [MiniGameObjectUtilityObject] $blueprint, [System.String] $id ) { $object = [MiniGameObjectUtilityObject]@{ id = $id _manager = $blueprint._manager } $object.canvas = $blueprint.canvas $object.SetPos($blueprint.position) $object.SetVel($blueprint.velocity) foreach ($entry in $blueprint.canvasStates.Keys) { $object.AddCanvas($entry, $blueprint.canvasStates[$entry]) } foreach ($tag in $blueprint.tag.Keys) { $object.Tag($tag) } foreach ($var in $blueprint.var.Keys) { $object.Set($var, $blueprint.var[$var]) } if ($blueprint.parent) { $object.SetParent($blueprint.parent) } if ($null -NE $blueprint._group) { $object.Group($blueprint._group) } if ($null -NE $blueprint._collisionObject) { $object.CollisionGroup($blueprint._group) foreach ($handler in $blueprint._collisionObject._onCollision.Values) { $object.OnCollision($handler) } } if ($null -NE $blueprint.Foreground._bytes) { $object.Foreground._bytes = $blueprint.Foreground._bytes } if ($blueprint.Background._bytes) { $object.Background._bytes = $blueprint.Background._bytes } if ($blueprint.Foreground._color) { $object.Foreground.SetColor($blueprint.Foreground._color) } if ($blueprint.Background._color) { $object.Background.SetColor($blueprint.Background._color) } if ($null -NE $blueprint._blueprintData) { $initData = $blueprint._blueprintData.initData $blueprint._blueprintData.onInit.Invoke($object, $initData) } return $object } [System.Boolean] $_isBlueprint = $false [System.Object] $_blueprintData = $null [void] OnBlueprintInit([System.Object] $initData, [scriptblock] $action) { $this._blueprintData = @{ onInit = $action initData = $initData } } [System.String] $_group [System.Object] $_manager [System.Object] $_collisionObject # Parent and child relations [MiniGameObjectUtilityObject] $parent = $null [System.Collections.Hashtable] $children = @{} # Tag and variable storage [System.Collections.Hashtable] $tag = @{} [System.Collections.Hashtable] $var = @{} # Information to draw the object [System.String] $id [System.Object] $canvas = @{ name = $null; data = @() } [System.Collections.Hashtable] $canvasStates = @{} [MiniGameObjectUtilityObjectGraphics] $Foreground = [MiniGameObjectUtilityObjectGraphics]@{} [MiniGameObjectUtilityObjectGraphics] $Background = [MiniGameObjectUtilityObjectGraphics]@{} [System.Numerics.Vector2] $velocity = [System.Numerics.Vector2]::Zero [System.Numerics.Vector2] $position = [System.Numerics.Vector2]::Zero [System.Numerics.Vector2] $offset = [System.Numerics.Vector2]::Zero <# Helper properties to correclty draw and undraw the object. - $_wasDrawn: Prevent undrawing with no valid data to undraw. - $_drawnCanvas: Keep track of the drawn state to correctly undraw again, when the current state has changed. #> [System.Boolean] $_wasDrawn = $false [System.String[]] $_drawnCanvas [System.Numerics.Vector2] $_drawnPosition <# Methods on the object. #> [void] Remove() { $this._manager.Remove($this.id) } [void] Group([System.String] $group) { $this._manager.Group($group, $this) } [void] Tag([System.String] $tag) { $this.tag.Remove($tag) $this.tag.Add($tag, $null) } [void] Untag([System.String] $tag) { $this.tag.Remove($tag) } [System.Boolean] HasTag([System.String[]] $tags) { foreach ($tag in $tags) { if (-NOT $this.tag.ContainsKey($tag)) { return $false } } return $true } <# Parent Child relations Child are drawn relative to its parents position #> [void] SetParent( [MiniGameObjectUtilityObject] $parent ) { $parent.children.Remove($this.id) $parent.children.Add($this.id, $this) $this.parent = $parent # This recalculates the position in offset to the parent. $this.SetPos($this.offset) } [void] UnsetParent() { if ($null -EQ $this.parent) { return } $this.parent.children.Remove($this.id) $this.parent = $null } [void] SetChild( [MiniGameObjectUtilityObject] $child ) { # Unset potential old parent, before setting a new one. $child.UnsetParent() $child.SetParent($this) } [void] UnsetChild( [MiniGameObjectUtilityObject] $child ) { $child.UnsetParent() } # Create a new object with automatic parent relation. [MiniGameObjectUtilityObject] AddChild( [System.String] $id ) { $object = $this._manager.Add($id) $object.SetParent($this) return $object } [MiniGameObjectUtilityObject] AddChild() { $randomId = $this._manager.RandomId() return $this.AddChild("$($this.id)-$randomId") } <# Drawing / Undrawing #> [void] Draw([System.Boolean] $force) { if (-NOT $this._wasDrawn -OR $force) { $this._wasDrawn = $true $this._drawnCanvas = $this.canvas.data $this._drawnPosition = $this.position $drawUtility = $this._manager.canvas $this.Foreground._draw($drawUtility.ForeGround) $this.Background._draw($drawUtility.Background) $drawUtility.Draw($this.position, $this.canvas.data) } foreach ($child in $this.children.Values) { $child.Draw($force) } } [void] Draw() { $this.Draw($false) } [void] Undraw() { if ($this._wasDrawn) { $this._wasDrawn = $false $this._manager.canvas.Undraw( $this._drawnPosition, $this._drawnCanvas ) } foreach ($child in $this.children.Values) { $child.Undraw() } } [void] Redraw() { $this.Undraw() $this.Draw() } <# Variables #> [System.Object] Get([System.String] $name) { return $this.var[$name] } [void] Set( [System.String] $name, [System.Object] $value ) { $this.var.Remove($name) $this.var.Add($name, $value) } [void] Add( [System.String] $name, [System.Object] $value ) { $this.var[$name] += $value } [void] Sub( [System.String] $name, [System.Object] $value ) { $this.var[$name] -= $value } <# Drawable Canvases #> [System.String] GetCanvas() { return $this.canvas.name } [void] SetCanvas([System.String] $name) { $this.canvas = @{ Name = $name Data = $this.canvasStates[$name] } if ($null -NE $this._collisionObject) { $this._collisionObject.SetCanvas($this.canvas.data) } } [void] SetCanvas() { $this.SetCanvas('__default__') } [void] AddCanvas( [System.String] $name, [System.String[]] $canvas ) { $this.canvasStates.Remove($name) $this.canvasStates.Add($name, $canvas) if ([System.String]::IsNullOrEmpty($this.canvas.name)) { $this.SetCanvas($name) } } [void] AddCanvas([System.String[]] $canvas) { $this.AddCanvas('__default__', $canvas) } <# Position and offset calculation offset: This is the position of the object in its 'reference-frame' position: This is the actual position of the object in the terminal. In Parent-Child relations the offset is relative to the parents actual position. #> [void] SetPos([System.Single] $x, [System.Single] $y) { $this.offset = [System.Numerics.Vector2]@{X = $x; Y = $y } if ($null -NE $this.parent) { $x = $this.parent.X + $x $y = $this.parent.Y + $y } $this.position = [System.Numerics.Vector2]@{X = $x; Y = $y } if ($null -NE $this._collisionObject) { $this._collisionObject.SetPos($x, $y) } foreach ($child in $this.children.Values) { $child.Position = [System.Numerics.Vector2]@{ X = $child.offset.X + $x Y = $child.offset.Y + $y } } } [void] SetPos([System.Numerics.Vector2] $pos) { $this.SetPos($pos.X, $pos.Y) } [void] Rightmost() { $this.SetPos($this._manager.canvas.Maximum.X, $this.position.Y) } [void] Leftmost() { $this.SetPos($this._manager.canvas.Minimum.X, $this.position.Y) } [void] Topmost() { $this.SetPos($this.position.X, $this._manager.canvas.Maximum.Y) } [void] Botmost() { $this.SetPos($this.position.X, $this._manager.canvas.Minimum.Y) } [void] SetVel($x, $y) { $this.velocity = [System.Numerics.Vector2]@{X = $x; Y = $y } } [void] SetVel([System.Numerics.Vector2] $vel) { $this.SetVel($vel.X, $vel.Y) } [void] Move() { if (0 -EQ $this.velocity.X -AND 0 -EQ $this.velocity.Y) { return } $this.Move($this.velocity.X, $this.velocity.Y) } [void] Move( [System.Single] $x, [System.Single] $y ) { if (0 -EQ $x -AND 0 -EQ $y) { return } $this.SetPos( $this.offset.X + $X, $this.offset.Y + $Y ) } [void] AddPos( [System.Single] $x, [System.Single] $y ) { $this.Move($x, $y) } [void] AddPos( [System.Numerics.Vector2] $pos ) { $this.Move($pos.X, $pos.Y) } [void] SubPos( [System.Single] $x, [System.Single] $y ) { $this.Move(-$x, - $y) } [void] SubPos( [System.Numerics.Vector2] $pos ) { $this.Move(-$pos.X, - $pos.Y) } [void] MulPos( [System.Single] $x, [System.Single] $y ) { $this.SetPos( $this.offset.X * $x, $this.offset.Y * $y ) } [void] MulPos( [System.Numerics.Vector2] $pos ) { $this.MulPos($pos.X, $pos.Y) } <# Collision groups and collision handlers #> [void] CollisionGroup([System.String] $group) { $this._collisionObject = $this._manager._collision.Add($this.id, $group) $this._collisionObject.SetPos($this.X, $this.Y) $this._collisionObject.SetCanvas($this.canvas) $this._collisionObject.SetRef($this) } [void] CollisionGroup() { $this._collisionObject = $this._manager._collision.Add($this.id) $this._collisionObject.SetPos($this.X, $this.Y) $this._collisionObject.SetCanvas($this.canvas) $this._collisionObject.SetRef($this) } [void] RemoveCollisionGroup() { if ($null -EQ $this._collisionObject) { return } $this._collisionObject.Remove() $this._collisionObject = $null } [void] OnCollision([scriptblock] $action) { $this._collisionObject.OnCollision($action) } [void] Collisions() { $this._collisionObject.Collisions() } } if ( $null -EQ (Get-TypeData -TypeName 'MiniGameObjectUtilityObject').Members.X ) { Update-TypeData -TypeName 'MiniGameObjectUtilityObject' -MemberName 'X' -Value { return $this.position.X } -MemberType ScriptProperty } if ( $null -EQ (Get-TypeData -TypeName 'MiniGameObjectUtilityObject').Members.Y ) { Update-TypeData -TypeName 'MiniGameObjectUtilityObject' -MemberName 'Y' -Value { return $this.position.Y } -MemberType ScriptProperty } <# /////////////////////////////////////////////////////////////////////// The Manager class for all objects. - Keeps track of all objects and ability to Add or Remove them. - Adds a blueprint-object for creating many similar objects. - Groups objects together. 'obstacles', 'stars', etc. - Perform collision calculation for all or a specific group. #> class MiniGameObjectUtility { [System.Collections.Hashtable] $_blueprints = @{ __default__ = [MiniGameObjectUtilityObject]@{ id = "blueprint-$id" _manager = $this _isBlueprint = $true } } <# Adds a blueprint-object for creating many similar objects. #> [MiniGameObjectUtilityObject] AddBlueprint([System.String] $id) { $object = [MiniGameObjectUtilityObject]@{ id = $id _manager = $this _isBlueprint = $true } $this._blueprints.Add($id, $object) return $object } <# Keeps track of all objects and ability to Add or Remove them. #> [System.Collections.Hashtable] $_allObjects = @{} [System.String] RandomId() { $bytes = (1..8 | ForEach-Object { [byte](Get-Random -Max 256) }) $randomId = [System.Convert]::ToHexString($bytes) if ($this._allObjects.ContainsKey($randomId)) { return $this.RandomId() } else { return $randomId } } [MiniGameObjectUtilityObject] Get([System.String] $id) { return $this._allObjects[$id] } [MiniGameObjectUtilityObject[]] Get() { return $this._allObjects.Values } <# Groups objects together. 'obstacles', 'stars', etc. object will be simultaneously stored in all objects and its group. #> [System.Collections.Hashtable] $_groups = @{} [void] Group( [System.String] $group, [MiniGameObjectUtilityObject] $object ) { # Remove from old group if it exists. if ($null -NE $object._group) { $this._groups[$object._group].Remove($object.id) } # Create new group if it does not exist. if (-NOT $this._groups.ContainsKey($group)) { $this._groups.Add($group, @{}) } $object._group = $group # Blueprint objects are not added to the collision objects, # but we save the group, for instances of the blueprint being added. if (-NOT $object._isBlueprint) { $this._groups[$group].Add($object.id, $object) } } [MiniGameObjectUtilityObject[]] GetGroup([System.String] $group) { return $this._groups[$group].Values } <# Adding a new blank object. #> [MiniGameObjectUtilityObject] Add( [System.String] $id, [MiniGameObjectUtilityObject] $blueprint ) { $object = [MiniGameObjectUtilityObject]::FromBlueprint($blueprint, $id) $this._allObjects.Add($id, $object) return $object } [MiniGameObjectUtilityObject] Add([System.String] $id) { return $this.Add($id, $this._blueprints.__default__) } [MiniGameObjectUtilityObject] Add() { return $this.Add($this.RandomId(), $this._blueprints.__default__) } <# Adding an object via blueprint id. #> [MiniGameObjectUtilityObject] FromBlueprint( [System.String] $blueprint, [System.String] $id ) { return $this.Add($id, $this._blueprints[$blueprint]) } [MiniGameObjectUtilityObject] FromBlueprint( [System.String] $blueprint ) { return $this.Add($this.RandomId(), $this._blueprints[$blueprint]) } <# Adding an object via blueprint object. #> [MiniGameObjectUtilityObject] FromBlueprint( [MiniGameObjectUtilityObject] $blueprint, [System.String] $id ) { return $this.Add($id, $blueprint) } [MiniGameObjectUtilityObject] FromBlueprint( [MiniGameObjectUtilityObject] $blueprint ) { return $this.Add($this.RandomId(), $blueprint) } <# Removing an object. #> [void] Remove([System.String] $id) { # Retrieve the object. $object = $this._allObjects[$id] # Remove any children from the object. foreach ($child in $object.children.Values) { $this.Remove($child.id) } # Remove it from all objects $this._allObjects.Remove($id) # Remove it from a potential group if ($null -NE $object._group) { $this._groups[$object._group].Remove($object.id) } # Remove it from the collision object $object.RemoveCollisionGroup() # Undraw it from the screen $object.Undraw() } [void] Remove([MiniGameObjectUtilityObject] $object) { $this.Remove($object.id) } <# Perform collision calculation for all or a specific group. #> [System.Object] $_collision = (New-MiniGameCollisionUtility -Method $CollisionMethod) [void] Collisions([System.String] $group) { $this._collision.Collisions($group) } [void] Collisions() { $this._collision.Collisions() } <# Calls the objects to Draw, Undraw, Redraw or Move #> [void] _process([System.Management.Automation.PSMethod] $method) { [System.String[]]$objectIds = $this._allObjects.Keys for ($index = 0; $index -LT $objectIds.Count; $index++) { $method.Invoke($objectIds[$index]) } } [void] Draw([System.String] $id) { $this._allObjects[$id].Draw() } [void] Undraw([System.String] $id) { $this._allObjects[$id].Undraw() } [void] Redraw([System.String] $id) { $this._allObjects[$id].Redraw() } [void] Move([System.String] $id) { $this._allObjects[$id].Move() } [void] Draw() { $this._process($this.Draw) } [void] Undraw() { $this._process($this.Undraw) } [void] Redraw() { $this._process($this.Redraw) } [void] Move() { $this._process($this.Move) } [System.Object] $canvas MiniGameObjectUtility([System.Object] $canvas) { $this.canvas = $canvas } } $drawSettings = @{ MinimumX = $MinimumX MinimumY = $MinimumY MaximumX = $MaximumX MaximumY = $MaximumY TrimSpaces = $TrimSpaces InvertY = $InvertY.IsPresent InvertX = $InvertX.IsPresent } $canvas = New-MiniGameDrawUtility @drawSettings return [MiniGameObjectUtility]::new($canvas) } |