New-TcMonoGameSpriteSheet.ps1
<#PSScriptInfo
.VERSION 1.0.0 .GUID a56f4e1c-b3c7-4956-a23a-7ffb0dd865ef .AUTHOR David Paulino .COMPANYNAME TugaCode .TAGS MonoGame .LICENSEURI https://github.com/uclobby/New-TcMonoGameSpriteSheet/blob/main/LICENSE .PROJECTURI https://github.com/uclobby/New-TcMonoGameSpriteSheet .DESCRIPTION PowerShell script that generates a MonoGame compatible Sprite Sheet with the image files in a given a folder. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path ) if (!(Test-Path $Path -PathType Container -ErrorAction SilentlyContinue)) { Write-Warning -Message "Invalid path: $path, please check it and try again." exit } Add-Type -AssemblyName System.Drawing $SpriteFolders = Get-ChildItem -Path $Path -Directory foreach ($SpritePath in $SpriteFolders) { $SpriteCount = 0 $AnimatedSpriteCount = 0 Write-Verbose "Processing Path $SpritePath" #Checking if we are handling a folder, if the Path is invalid we return false. if (!(Test-Path -Path $SpritePath -PathType Container)) { return } $spriteImages = Get-ChildItem -Path $SpritePath -Recurse -Depth 4 -Filter "*.png" if ($spriteImages.count -eq 0) { Write-Warning "No Sprites found in: $SpritePath" return } #Partial Path so we can use it for the atlas file and the xml. $OutputPartialPath = (Split-Path $SpritePath -Parent) + "\atlas-" + (Split-Path $SpritePath -Leaf).ToLower() Write-Verbose "Output partial path: $OutputPartialPath" #region Texture Atlas XML initialization. $outMonoGameXml = New-Object System.Xml.XmlDocument $xmlRootElement = $outMonoGameXml.CreateElement("TextureAtlas") [void]$outMonoGameXml.AppendChild($xmlRootElement) $xmlDecl = $outMonoGameXml.CreateXmlDeclaration("1.0", "utf-8", $null) [void]$outMonoGameXml.InsertBefore($xmlDecl, $xmlRootElement) $xmlElement = $outMonoGameXml.CreateElement("Texture") $xmlElement.InnerText = (Split-Path -Path (Split-Path $SpritePath -Parent) -leaf) + "\" + [System.IO.Path]::GetFileNameWithoutExtension($OutputPartialPath) [void]$xmlRootElement.AppendChild($xmlElement) $xmlRegions = $outMonoGameXml.CreateElement("Regions") $xmlAnimations = $outMonoGameXml.CreateElement("Animations") #endRegion #TODO: Add support for different sizes sprites. #We use the first sprite to get the height and width. $sampleSprite = [System.Drawing.Image]::FromFile($spriteImages[0].FullName) $SpriteWidth = $sampleSprite.Width $SpriteHeight = $sampleSprite.Height Write-Verbose -Message "Using Sprite size $SpriteWidth`x$SpriteHeight" #Number of rows we need, for the sprite per row we can do floor of the sqrt of number of sprites. For number of rows we can use ceiling. $tmpSqrt = [math]::Sqrt($spriteImages.Count) $tmpSpritesPerRow = [math]::Ceiling($tmpSqrt) #Calculating the next power of 2 $outWidth = [int][Math]::Pow(2, [math]::Ceiling([math]::Log($tmpSpritesPerRow * $SpriteWidth) / [math]::Log(2))) $SpritesPerRow = [math]::Floor($outWidth / $SpriteWidth) $hasAnimatedSprites = $false $AnimatedSpritesCycle = $false $lastSpriteName = "" $drawWidth = 0 $drawHeight = 0 $drawSpriteInRowCount = 0 Write-Verbose -Message "Creating output with $outWidth`x$outWidth and $SpritesPerRow sprites per row." $outBitmap = New-Object System.Drawing.Bitmap($outWidth, $outWidth) $outGraphics = [System.Drawing.Graphics]::FromImage($outBitmap) foreach ($spriteImg in $spriteImages) { $currentSprite = [System.Drawing.Image]::FromFile($spriteImg.FullName) $tmpSpriteName = ([System.IO.Path]::GetFileNameWithoutExtension($spriteImg)).ToLower() $tmpSNS = $tmpSpriteName.Split('-'); $spriteName = $tmpSNS[0] if (($currentSprite.Height -ne $SpriteHeight) -or ($currentSprite.Width -ne $SpriteWidth)) { Write-Warning -Message ("Skiping sprite $tmpSpriteName because the size " + $currentSprite.Width + "x" + $currentSprite.Height + " is different from what is expected $SpriteWidth`x$SpriteHeight") continue } $outGraphics.DrawImage($currentSprite, $drawWidth, $drawHeight) $currentSprite.Dispose() $xmlElement = $outMonoGameXml.CreateElement("Region") #If we have a filename different from the last, then we need to add the XML animation element to XML document. if (($lastSpriteName -ne $spriteName) -and $AnimatedSpritesCycle) { $AnimatedSpritesCycle = $false [void]$xmlAnimations.AppendChild($xmlAnimationElement) $AnimatedSpriteCount++ } if ($tmpSNS.Count -gt 1 ) { $hasAnimatedSprites = $true $spriteName = $tmpSNS[0] + "-" + $tmpSNS[1] $xmlFrameElement = $outMonoGameXml.CreateElement("Frame") $xmlFrameElement.SetAttribute("region", $spriteName) if ($lastSpriteName -ne $tmpSNS[0]) { $AnimatedSpritesCycle = $true $xmlAnimationElement = $outMonoGameXml.CreateElement("Animation") $xmlAnimationElement.SetAttribute("name", $tmpSNS[0] + "-animation") $xmlAnimationElement.SetAttribute("delay", 200) } [void]$xmlAnimationElement.AppendChild($xmlFrameElement) $lastSpriteName = $tmpSNS[0] } $xmlElement.SetAttribute("name", $spriteName) $xmlElement.SetAttribute("x", $drawWidth) $xmlElement.SetAttribute("y", $drawHeight) $xmlElement.SetAttribute("width", $SpriteWidth) $xmlElement.SetAttribute("height", $SpriteHeight) [void]$xmlRegions.AppendChild($xmlElement) $drawWidth += $SpriteWidth $drawSpriteInRowCount++ $SpriteCount++ #If we reache the number of sprites per row we need to update the draw height. if ($drawSpriteInRowCount -eq $SpritesPerRow) { $drawSpriteInRowCount = 0 $drawWidth = 0 $drawHeight += $SpriteHeight } } #We need to make sure to add this in case that the last sprite was part of an animated sprite. if ($AnimatedSpritesCycle) { [void]$xmlAnimations.AppendChild($xmlAnimationElement) $AnimatedSpriteCount++ } $outBitmap.Save($OutputPartialPath + ".png", [System.Drawing.Imaging.ImageFormat]::Png) $outGraphics.Dispose() $outBitmap.Dispose() [void]$xmlRootElement.AppendChild($xmlRegions) if ($hasAnimatedSprites) { [void]$xmlRootElement.AppendChild($xmlAnimations) } $outMonoGameXml.Save($OutputPartialPath + "-definition.xml") Write-Host -Message ("Atlas/Sprite sheet and XML available in : " + (Split-Path $SpritePath -Parent) + [Environment]::NewLine + "$SpriteCount sprite(s)" + [Environment]::NewLine + "$AnimatedSpriteCount animated sprite(s)" + [Environment]::NewLine) -ForegroundColor Cyan } |