I made a script that converts a Photoshop document to AppGameKit code + graphic. Supports animation too! I think this is convenient since I can do the entire graphic design in Photoshop, then run the script and I have the stuff ready to paste into AppGameKit, without bothering with any middle software.
However ...
I heard that my script doesn't run on Photoshop CC. But I'm using Photoshop CS6, so I can't troubleshoot this.
If anyone runs Photoshop CC, and would take a look at the script, it would be really great!
(Runs fine on CS6, for those who still use that.)
#target photoshop
/******************************************************************************
FILE: psd2agk-1.06.jsx
VERSION: 1.06
WRITTEN: 2017
psd2agk-1.06.jsx is a Photoshop script used to save toplevel layersets as sprites for App Game Kit.
The original x/y position and depth of the layers are maintained in AGK2, and spritesheet
animations are automatically generated.
PREREQUIREMENTS:
You need to have Photoshop installed, of course. I have tested the script on CS6. The AGK file
generated by the script are Tier 1 (BASIC) using Virtual Resolution.
HOW TO USE:
To test it this script, first open game-graphic.psd in Photoshop. Then double-click psd2agk.jsx to
execute it. You’ll get a warning. Click OK. Now Photoshop will be super buzy as it saves each
layerset as a sprite.
When Photoshop is done, the script has saved the sprites as .png files in a subfolder called
Output. This folder must always be located at the same directory as the .psd file you run time
script on.
It has also made a file called generatedLoader.agc which you will use to load your sprites.
HOW TO ADD THE GENERATED FILES TO AGK:
1) Open your AGK project. (If it is a new project, save it now.) Then select File > New File give it
the name generatedLoader.agc and click Save. (You only need this the first time you add the
graphic to this project)
2) In AGK, add the following lines to main.agc:
#include "generatedLoader.agc"
Gosub autoLoad
2) from your ’Output’ directory, copy generatedLoader.agc to your AGK project directory,
overwriting the old file.
3) Copy the .png files from your ’Output’ folder to your projects ’/media’ folder.
ADVANCED FEATURES:
To adjust settings, you can open psd2agk-1.06.jsx in a text editor.
The toplevel layersets are simply merged into sprites, so you can have all kind of layers or
layersets inside them. For instance, take a look at all the crap in my ’backdrop’ layerset in
game-graphics.psd
If the layersets filename contains the phrase Ani, it will treat all layersets inside it as frames in a
animation and generate a spritesheet. Place the earliest frames at the top of the layer stack.
If the layersets name contain two slashes //, it is considered a comment layer and is ignored.
Layersets that share same name are considered to be sprites that share the same image. This
means that you can duplicate the layerset, and the script will cleverly recycle the same image
file. Lets say you duplicate a layerset called buildingBlock so you have 4 layersets:
buildingBlock copy 3
buildingBlock copy 2
buildingBlock copy
buildingBlock
They will all be using a single image file. (that copy stuff is cleverly ignored)
FINAL NOTES:
The generated .png and .agc files overwrites existing files in the Output directory. Just so
you know.
For each generated sprite, all layersets must be turned off, one at a time. This means that the
script execution time will grow exponentially with each extra layerset: If you have 31 layersets,
Photoshop will need to toggle layerset visibility 31*31 = 961 times. (The script is thoughtful
enough to give a tiny beep when it’s done)
******************************************************************************/
/******************************************************************************
SETTINGS
******************************************************************************/
// How many rows the spritesheets should contain.
// Set it to a number. Default is 8
imgPrRow = 8;
// Name of folder to save in, relative to the .psd file location
outputFolderName = "Output";
// Beep when the script is finished
// Set it to true or false
beepWhenFinished = true;
// Use this to scale down the output sprites.
// 1.0 means no rescaling.
// If you like to work on double scale originals, set the finalGraphicScale to 0.5
finalGraphicScale = 1.0;
// Type of resampling used if you size down the graphic.
// Set it to 1, 2 or 3:
// 1: NONE
// 2: BICUBIC
// 3: BILINEAR
scaleResampling = 2;
// Save using the tinyPNG plugin?
// Set it to true or false
// NOTE: If set to true, tinyPNG needs to be installed
saveUsingTinyPNG = false;
// Do you want to execute an action on each sprite?
// Set it to true or false
// NOTE: Avoid actions which change the sprite size. Stick to basic)
// NOTE: Write "noAction" in the layerset name to not execute action on the sprite
doPsAction = false;
// The title of your action folder:
actionFolderTitle = "x";
// The title of your action:
actionTitle = "Ghost";
// Decides if the agk code should specify that the reference is an integer
// needed if you go #option_explicit but otherwise it kinda just clutters
// your code. An AGK example is seen in the next two lines:
// scaryGhostImg as integer
// scaryGhostImg = LoadImage("scaryGhost.png")
specifyAsInteger = false
/******************************************************************************
INTRO
******************************************************************************/
var linialBackup = preferences.rulerUnits; // Settings saved
preferences.rulerUnits = Units.PIXELS; // Settings changed
layerNameArray = new Array();
// The script will call the original .psd document 'docRefSource'. Then it creates a duplicate name 'docRefCopy' which it resize and do stuff with. Finally, it creates a 'docRef' duplicate of each sprite.
if (app.documents.length == 0) alert("You need to have an image open in Photoshop for this script to work!");
else {
docRefSource = app.activeDocument;
var mappe = Folder(docRefSource.path + "/" + outputFolderName + "/");
if (mappe instanceof Folder && mappe.exists) {
outFit = "";
listOfGraphic = "";
countingRepeatedGraphic = 1;
theSpriteDepth = 100;
docRefCopy = docRefSource.duplicate();
app.activeDocument = docRefCopy;
// Size the shit down
if (finalGraphicScale != 1.0) {
if (scaleResampling == 1) docRefCopy.resizeImage(docRefCopy.width*finalGraphicScale, docRefCopy.height*finalGraphicScale, null, ResampleMethod.NONE);
else if (scaleResampling == 2) docRefCopy.resizeImage(docRefCopy.width*finalGraphicScale, docRefCopy.height*finalGraphicScale, null, ResampleMethod.BICUBIC);
else if (scaleResampling == 3) docRefCopy.resizeImage(docRefCopy.width*finalGraphicScale, docRefCopy.height*finalGraphicScale, null, ResampleMethod.BILINEAR);
else alert("Wrong value in scaleResamling.");
}
orgWidth = docRefCopy.width.value; // Log the width and height
orgHeight = docRefCopy.height.value;
/******************************************************************************
RUN ON EACH VISIBLE LAYER
******************************************************************************/
for (var firstLvlLayerRef = 0; firstLvlLayerRef < docRefCopy.layers.length; firstLvlLayerRef++){
docRefCopy.activeLayer = docRefCopy.layers[firstLvlLayerRef];
if (docRefCopy.activeLayer.name.indexOf("//") == -1) { //Only run if the layer is not a comment
alreadyUsed = false; // Determine if the name is already in the array
for (i = 0; i < layerNameArray.length; i++) {
if (layerNameArray[i] == removeCrapText(docRefCopy.activeLayer.name)) alreadyUsed = true;
}
//alert ("" + docRefCopy.activeLayer.name + " is already used: " + alreadyUsed);
layerNameArray.push(removeCrapText(docRefCopy.activeLayer.name)); // push a cleaned version of the layer name into the array
if (docRefCopy.layers[firstLvlLayerRef].typename == "LayerSet" && docRefCopy.layers[firstLvlLayerRef].name.indexOf("Ani") != -1) {
if (alreadyUsed) repeatSpriteSheet();
else mkSpriteSheet ();
}
else {
if (alreadyUsed) layerRepeat ();
else layerToPng ();
}
}
}
docRefCopy.close(SaveOptions.DONOTSAVECHANGES);
/******************************************************************************
GENERATE TEXT
******************************************************************************/
outFit = listOfGraphic + "\n\n\n\n" + outFit;
outFit = "// Generated text here\n\nautoLoad:\n\n" + outFit + "\nReturn\n";
var targ = new File(docRefSource.path + "/" + outputFolderName + "/generatedLoader.agc");
targ.open("w");
targ.write(outFit);
targ.close();
if (beepWhenFinished) beep();
} else alert("The folder " + docRefSource.path + "/" + outputFolderName + "/" + " does not exist.");
} // else
/******************************************************************************
EXIT
******************************************************************************/
preferences.rulerUnits = linialBackup; // Settings restored
/******************************************************************************
FUNCTIONS
******************************************************************************/
function removeCrapText (layerNameText) {
if (layerNameText.indexOf("noAction") != -1) layerNameText = layerNameText.split("noAction").join("");
if (layerNameText.indexOf(" copy") != -1) {
for (b = 200; b > 1; b--) {
layerNameText = layerNameText.split(" copy " + b).join("");
}
layerNameText = layerNameText.split(" copy").join("");
}
layerNameText = layerNameText.split(" ").join("");
layerNameText = layerNameText.split(" ").join("");
layerNameText = layerNameText.split(" ").join("");
layerNameText = layerNameText.split(" ").join("");
layerNameText = layerNameText.split(" ").join("");
layerNameText = layerNameText.split(" ").join("");
layerNameText = layerNameText.split(" ").join("");
layerNameText = layerNameText.split(" ").join("");
layerNameText = layerNameText.split(" ").join("");
layerNameText = layerNameText.split(" ").join("");
layerNameText = layerNameText.split(" ").join("");
layerNameText = layerNameText.split(" ").join("");
return layerNameText;
}
function layerToPng () {
// This function will save the layer as a .png and generate AGK code
docRef = docRefCopy.duplicate();
app.activeDocument = docRef;
layerRef = docRef.activeLayer;
for (var mm = 0; mm < docRef.layers.length; mm++){ // Turn all layers and layer sets invisible
docRef.layers[mm].visible = false;
}
layerRef.visible = true; // Turn our active layer visible again
docRef.trim(TrimType.TRANSPARENT, true, true, false, false); // Crop top and left
topOffset = orgHeight - docRef.height.value;
leftOffset = orgWidth - docRef.width.value;
layerName = removeCrapText(layerRef.name);
// Generate AGK code
if (specifyAsInteger) listOfGraphic = listOfGraphic + layerName + "Img as integer\n";
listOfGraphic = listOfGraphic + layerName + "Img = LoadImage(\"" + layerName + ".png\")\n";
outFitAdd = "";
if (specifyAsInteger) outFitAdd = outFitAdd + layerName + "Spr as integer\n";
outFitAdd = outFitAdd + layerName + "Spr = CreateSprite(" + layerName + "Img)\n";
outFitAdd = outFitAdd + "SetSpritePosition ( " + layerName + "Spr, " + leftOffset + ", " + topOffset + " )\n";
outFitAdd = outFitAdd + "SetSpriteDepth ( " + layerName + "Spr, " + theSpriteDepth + " )\n\n";
outFit = outFitAdd + outFit;
countingRepeatedGraphic = countingRepeatedGraphic + 1;
theSpriteDepth = theSpriteDepth + 10;
// Trim
docRef.trim(TrimType.TRANSPARENT, true, true, true, true); // Trim all
// Save as PNG and close
if (saveUsingTinyPNG) saveTinyPNG(docRef);
else saveAsPNG (docRef);
} // end function layerToPng
function layerRepeat () {
// This function will save some AGK code but does not save as a new image file
docRef = docRefCopy.duplicate();
app.activeDocument = docRef;
layerRef = docRef.activeLayer;
for (var mm = 0; mm < docRef.layers.length; mm++){ // Turn all layers and layer sets invisible
docRef.layers[mm].visible = false;
}
layerRef.visible = true; // Turn our active layer visible again
docRef.trim(TrimType.TRANSPARENT, true, true, false, false); // Crop top and left
topOffset = orgHeight - docRef.height.value;
leftOffset = orgWidth - docRef.width.value;
layerName = removeCrapText(layerRef.name);
// Generate AGK code
outFitAdd = "";
if (specifyAsInteger) outFitAdd = outFitAdd + layerName + "Spr" + countingRepeatedGraphic + " as integer\n";
outFitAdd = outFitAdd + layerName + "Spr" + countingRepeatedGraphic + " = CreateSprite(" + layerName + "Img)\n";
outFitAdd = outFitAdd + "SetSpritePosition ( " + layerName + "Spr" + countingRepeatedGraphic + ", " + leftOffset + ", " + topOffset + " )\n";
outFitAdd = outFitAdd + "SetSpriteDepth ( " + layerName + "Spr" + countingRepeatedGraphic + ", " + theSpriteDepth + " )\n\n";
outFit = outFitAdd + outFit;
countingRepeatedGraphic = countingRepeatedGraphic + 1;
theSpriteDepth = theSpriteDepth + 10;
docRef.close(SaveOptions.DONOTSAVECHANGES);
} // end function layerRepeat
function mkSpriteSheet () {
// generates a spritesheet
docRef = docRefCopy.duplicate();
app.activeDocument = docRef;
layerRef = docRef.activeLayer;
for (var mm = 0; mm < docRef.layers.length; mm++){ // Turn all layers and layer sets invisible
docRef.layers[mm].visible = false;
}
layerRef.visible = true; // Turn our active layer visible again
docRef.trim(TrimType.TRANSPARENT, true, true, false, false); // Crop top and left
topOffset = orgHeight - docRef.height.value;
leftOffset = orgWidth - docRef.width.value;
layerName = removeCrapText(layerRef.name);
NumberOfSprites = 0;
for (var m = 0; m < layerRef.layers.length; m++){
NumberOfSprites = NumberOfSprites + 1;
}
// Trim
docRef.trim(TrimType.TRANSPARENT, true, true, true, true); // Trim all
spriteWidth = docRef.width.value; // Log the width and height
spriteHeight = docRef.height.value;
// Generate AGK code
if (specifyAsInteger) listOfGraphic = listOfGraphic + layerName + "Img as integer\n";
listOfGraphic = listOfGraphic + layerName + "Img = LoadImage(\"" + layerName + ".png\")\n";
outFitAdd = "";
if (specifyAsInteger) outFitAdd = outFitAdd + layerName + "Spr as integer\n";
outFitAdd = outFitAdd + layerName + "Spr = CreateSprite(" + layerName + "Img)\n";
outFitAdd = outFitAdd + "SetSpritePosition ( " + layerName + "Spr, " + leftOffset + ", " + topOffset + " )\n";
outFitAdd = outFitAdd + "SetSpriteAnimation ( " + layerName + "Spr, " + spriteWidth + ", " + spriteHeight + ", " + NumberOfSprites + " )\n";
outFitAdd = outFitAdd + "SetSpriteDepth ( " + layerName + "Spr, " + theSpriteDepth + " )\n\n";
outFit = outFitAdd + outFit;
theSpriteDepth = theSpriteDepth + 10;
var col = Math.ceil( NumberOfSprites / imgPrRow );
spriteSheetWidth = 0;
if (imgPrRow > NumberOfSprites) {
spriteSheetWidth = NumberOfSprites*spriteWidth;
}
else {
spriteSheetWidth = imgPrRow*spriteWidth;
}
var SpriteSheetDocRef = documents.add(spriteSheetWidth, spriteHeight*col, 72.0, "Spritesheet generated by simpleSpriteSheet.jsx", NewDocumentMode.RGB, DocumentFill.TRANSPARENT);
var yPos = 0; // Numbers used to place the frames
var xPos = 0;
for (var m = 0; m < layerRef.layers.length; m++){
//alert(firstLvlLayerRef.layers.length + " " + firstLvlLayerRef.layers[m].name);
app.activeDocument = docRef;
docRef.activeLayer = layerRef.layers[m];
layerRef.layers[m].duplicate( SpriteSheetDocRef );
app.activeDocument = SpriteSheetDocRef;
positionLayer( SpriteSheetDocRef.activeLayer, xPos*spriteWidth, yPos*spriteHeight );
xPos++;
if (xPos >= imgPrRow) {
xPos=0;
yPos++;
}
}
// Save as PNG and close
if (saveUsingTinyPNG) saveTinyPNG(SpriteSheetDocRef);
else saveAsPNG (SpriteSheetDocRef);
docRef.close(SaveOptions.DONOTSAVECHANGES);
} // function mkSpriteSheet
function repeatSpriteSheet () {
// repeats a spritesheet
docRef = docRefCopy.duplicate();
app.activeDocument = docRef;
layerRef = docRef.activeLayer;
for (var mm = 0; mm < docRef.layers.length; mm++){ // Turn all layers and layer sets invisible
docRef.layers[mm].visible = false;
}
layerRef.visible = true; // Turn our active layer visible again
docRef.trim(TrimType.TRANSPARENT, true, true, false, false); // Crop top and left
topOffset = orgHeight - docRef.height.value;
leftOffset = orgWidth - docRef.width.value;
layerName = removeCrapText(layerRef.name);
NumberOfSprites = 0;
for (var m = 0; m < layerRef.layers.length; m++){
NumberOfSprites = NumberOfSprites + 1;
}
// Trim
docRef.trim(TrimType.TRANSPARENT, true, true, true, true); // Trim all
spriteWidth = docRef.width.value; // Log the width and height
spriteHeight = docRef.height.value;
// Generate AGK code
outFitAdd = "";
if (specifyAsInteger) outFitAdd = outFitAdd + layerName + "Spr" + countingRepeatedGraphic + " as integer\n";
outFitAdd = outFitAdd + layerName + "Spr" + countingRepeatedGraphic + " = CreateSprite(" + layerName + "Img)\n";
outFitAdd = outFitAdd + "SetSpritePosition ( " + layerName + "Spr" + countingRepeatedGraphic + ", " + leftOffset + ", " + topOffset + " )\n";
outFitAdd = outFitAdd + "SetSpriteAnimation ( " + layerName + "Spr" + countingRepeatedGraphic + ", " + spriteWidth + ", " + spriteHeight + ", " + NumberOfSprites + " )\n\n";
outFit = outFitAdd + outFit;
countingRepeatedGraphic = countingRepeatedGraphic + 1;
docRef.close(SaveOptions.DONOTSAVECHANGES);
} // function repeatSpriteSheet
function positionLayer( lyr, positionLayerX, positionLayerY ){ // layerObject, Number, Number
// if can not move layer return
if(lyr.iisBackgroundLayer||lyr.positionLocked)
{
alert("Background!!!");
return
}
lyr.translate (positionLayerX, positionLayerY);
}
function saveAsPNG (docRefSave) {
if (docRefSave.activeLayer.name.indexOf ("noAction") != -1) doActionOnSprite = false;
else doActionOnSprite = true;
var dummyVisibleLayer = docRefSave.artLayers.add(); // Photoshop must have a visible layer selected
docRefSave.activeLayer = dummyVisibleLayer
docRefSave.mergeVisibleLayers() // Merge the layers.
for (i = docRefSave.layers.length-1; i >=0; i--) { // Remove all empty layers.
if (!docRefSave.layers[i].visible) {
docRefSave.layers[i].remove()
}
}
if (doPsAction && doActionOnSprite) {
doAction(actionTitle, actionFolderTitle);
var dummyVisibleLayer = docRefSave.artLayers.add(); // Photoshop must have a visible layer selected
docRefSave.activeLayer = dummyVisibleLayer
docRefSave.mergeVisibleLayers() // Merge the layers.
}
pngOptions = new PNGSaveOptions() // Set up PNG save options.
pngOptions.compression = 0
pngOptions.interlaced = false
savePath = File(docRefSource.path + "/" + outputFolderName + "/" + layerName + ".png"); // Set up destination path.
docRefSave.saveAs(savePath, pngOptions, false, Extension.NONE); // Save!
docRefSave.close(SaveOptions.DONOTSAVECHANGES);
}
function saveTinyPNG(tinyPNGdoc) {
if (tinyPNGdoc.activeLayer.name.indexOf ("noAction") != -1) doActionOnSprite = false;
else doActionOnSprite = true;
if (tinyPNGdoc.mode == DocumentMode.INDEXEDCOLOR) {
tinyPNGdoc.changeMode(ChangeMode.RGB);
}
if (tinyPNGdoc.bitsPerChannel == BitsPerChannelType.SIXTEEN) {
convertBitDepth(8);
}
var dummyVisibleLayer = tinyPNGdoc.artLayers.add(); // Photoshop must have a visible layer selected
tinyPNGdoc.activeLayer = dummyVisibleLayer
tinyPNGdoc.mergeVisibleLayers() // Merge the layers.
for (i = tinyPNGdoc.layers.length-1; i >=0; i--) { // Remove all empty layers.
if (!tinyPNGdoc.layers[i].visible) {
tinyPNGdoc.layers[i].remove()
}
}
if (doPsAction && doActionOnSprite) {
doAction(actionTitle, actionFolderTitle);
var dummyVisibleLayer = tinyPNGdoc.artLayers.add(); // Photoshop must have a visible layer selected
tinyPNGdoc.activeLayer = dummyVisibleLayer
tinyPNGdoc.mergeVisibleLayers() // Merge the layers.
}
savePath = File(docRefSource.path + "/" + outputFolderName + "/" + layerName + ".png"); // Set up destination path.
var type = charIDToTypeID("tyPN"); /* tyJP for JPEG */
var percentage = 100;
var tinypng = new ActionDescriptor();
tinypng.putPath(charIDToTypeID("In "), savePath); /* Overwrite original! */
tinypng.putEnumerated(charIDToTypeID("FlTy"), charIDToTypeID("tyFT"), type);
tinypng.putUnitDouble(charIDToTypeID("Scl "), charIDToTypeID("#Prc"), percentage );
var compress = new ActionDescriptor();
compress.putObject(charIDToTypeID("Usng"), charIDToTypeID("tinY"), tinypng);
executeAction(charIDToTypeID("Expr"), compress, DialogModes.NO);
tinyPNGdoc.close(SaveOptions.DONOTSAVECHANGES);
}
function convertBitDepth(bitdepth) {
var id1 = charIDToTypeID("CnvM");
var convert = new ActionDescriptor();
var id2 = charIDToTypeID("Dpth");
convert.putInteger(id2, bitdepth);
executeAction(id1, convert, DialogModes.NO);
}