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
}