Video game asset pipeline with CMake: XCode integration

Daniele's picture

A simple task as building an XCode External Target, can require various scripts to be accomplished when you are building assets. Following my previous post Video game asset pipeline with CMake, I will show how the asset pipeline can be integrated with XCode so that it can be automatically processed also during your normal builds.

To integrate the asset pipeline with XCode we need a couple of things: an External Target that will start our custom asset pipeline and a shell script to copy generated assets to the final application bundle.

The Assets External Target

An External Target is a special XCode target that, instead of being managed directly by XCode, its building is entrusted to an external tool (e.g. make or cmake). This is what XCode needs to run our CMake-based asset pipeline.

To create the "Assets External Target": from the "Project" menu select the option "New Target".

In the window that will open, choose the "External Target" template; then fill the "Custom Build Command" settings as shown here:

XCode External Target settings

At each build of the Assets External Target we want to call cmake to keep up-to date the Makefiles needed by the asset pipeline, and in turns call make to process them. Since XCode cannot do both tasks at once, we will use an intermediate Makefile (stored in the same directory of the XCode project, the one containing the <project>.xcodeproj directory). The content of Makefile.assets is very simple:

build:
	-mkdir -p "${TARGET_TEMP_DIR}"
	export PATH="${PATH}:${PATH_AUX}" ; cd "${TARGET_TEMP_DIR}" && cmake -G "Unix Makefiles" "${SOURCE_ROOT}/../assets" && make -j 2 VERBOSE=1
 
clean:
	-cd "${TARGET_TEMP_DIR}" && make clean
	-rm -fr "${TARGET_TEMP_DIR}"
 
.PHONY: build clean

TARGET_TEMP_DIR is an XCode variable that identifies the directory containing the target's intermediate build files; this is where our asset pipeline will build and store the processed assets. Later we will copy them to the application bundle.

Automatic execution on project building

To automatically build your assets when you build your main project, you have to add the newly created "Assets External Target" as dependency to your main target: open your main target's project info (right click your main target item in the "Targets" Smart Group, choose "Get Info"), under the "General" tab add to the list of "Direct Dependencies" the "Assets" target.

Adding the Asset External Target as dependency of the main target

Now, when you build your project, all assets that are out-of-sync will be automatically rebuilt. The last thing to do is copy them to your application bundle.

Copy assets to the application bundle

When the asset pipeline builds, it stores the output files to $TARGET_TEMP_DIR. We need to copy these files to the application bundle. With "static" resources this is done automatically by XCode, but with our custom resources (that are unknown to XCode) we have to do it ourselves.

This should be a very simple task, but as some assets have to be further processed when deployed to the physical iPhone device (as pre-multiplying alpha channel of PNG images) we will use a shell script to help us in this task.

A script to do it follows; name it copyAssets and store it into the same directory of the XCode project. In order, it does:

  1. checks for the existence of needed directories;
  2. retrieves needed tools;
  3. copies normal resources;
  4. if deploying to a device, pre-multiplies PNG images, otherwise simply copies them;
  5. validates, converts to binary format and copies all PLIST files.

#!/bin/sh
 
if [ -z "$ASSETS_DIR" ]; then
	echo 'Variable $ASSETS_DIR not setted' >&2
	exit 1
fi
if [ ! -d $ASSETS_DIR ]; then
	echo "Directory '$ASSETS_DIR' not found" >&2
	exit 1
fi
 
if [ -z "$CONFIGURATION_BUILD_DIR" ]; then
	echo 'Variable $CONFIGURATION_BUILD_DIR not setted' >&2
	exit 1
fi
if [ -z "$UNLOCALIZED_RESOURCES_FOLDER_PATH" ]; then
	echo 'Variable $UNLOCALIZED_RESOURCES_FOLDER_PATH not setted' >&2
	exit 1
fi
if [ ! -d $CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH ]; then
	echo "Directory '$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH' not found" >&2
	exit 1
fi
 
COPY_COMMAND="$DEVELOPER_LIBRARY_DIR/PrivateFrameworks/DevToolsCore.framework/Resources/pbxcp"
if [ ! -x $COPY_COMMAND ]; then
	echo "'$COPY_COMMAND' not found" >&2
	exit 1
fi
 
COPY_PLIST_COMMAND="$DEVELOPER_LIBRARY_DIR/Xcode/Plug-ins/CoreBuildTasks.xcplugin/Contents/Resources/copyplist"
if [ ! -x $COPY_PLIST_COMMAND ]; then
	echo "'$COPY_PLIST_COMMAND' not found" >&2
	exit 1
fi
# Needed for a bug in 'copyplist', see later.
PLUTIL_COMMAND="/usr/bin/plutil"
if [ ! -x $PLUTIL_COMMAND ]; then
	echo "'$PLUTIL_COMMAND' not found" >&2
	exit 1
fi
 
# If not exists, it should means we are building for the simulator
COPY_PNG_COMMAND="$PLATFORM_DEVELOPER_LIBRARY_DIR/Xcode/Plug-ins/iPhoneOS Build System Support.xcplugin/Contents/Resources/copypng"
if [[ ! -x $COPY_PNG_COMMAND && "$PLATFORM_NAME" = 'iphoneos' ]]; then
	echo "'$COPY_PNG_COMMAND' not found" >&2
	exit 1
fi
 
NORMAL_RESOURCES=$( find ${ASSETS_DIR} -type f | grep -v '\.\(png\|plist\)$' )
echo "$NORMAL_RESOURCES" | while IFS='\n' read FILE; do
	"$COPY_COMMAND" -resolve-src-symlinks "$FILE" "$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH"
done
 
PNGs=$( find ${ASSETS_DIR} -type f -name *.png )
echo "$PNGs" | while IFS='\n' read FILE; do
	if [ -x "$COPY_PNG_COMMAND" ]; then
		filename=$( basename "$FILE" )
		"$COPY_PNG_COMMAND" -compress "" "$FILE" "$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/$filename"
	else
		"$COPY_COMMAND" -resolve-src-symlinks "$FILE" "$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH"
	fi
done
 
PLISTs=$( find ${ASSETS_DIR} -type f -name *.plist )
echo "$PLISTs" | while IFS='\n' read FILE; do
	# 'copyplist --validate' is bugged: 'plutil' wants '-s' option after '-lint' one.
	#"$COPY_PLIST_COMMAND" --validate --convert binary1 "$FILE" --outdir "$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH"
	"$PLUTIL_COMMAND" -lint -s -- "$FILE" && "$COPY_PLIST_COMMAND" --convert binary1 "$FILE" --outdir "$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH"
done

All input environment variables, excluded ASSETS_DIR, are standard XCode variables that store the used build directories. Using them, we can check for the needed tools and identify the right input/output directories (e.g. the application bundle is at $CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH). ASSETS_DIR is a custom variable that stores the directory containing all generated assets (a subdirectory of the previous $TARGET_TEMP_DIR, we will use it soon).

As the application bundle directory is created as part of the main target building (and thus do not exists when we build the Assets target), we have to execute copyAssets script as a custom build phase of the main target: right click on your main target, choose "Add", "New Build Phase", "New Run Script Build Phase"; in the window that will open, under the "General" tag, set the used shell to "/bin/sh", and in the script text area paste:

ASSETS_DIR=${CONFIGURATION_TEMP_DIR}/Assets.build/output ${SOURCE_ROOT}/copyAssets

To be safe, drag this custom build phase after the standard ones.

ASSETS_DIR, as shown in the previous snippet, assumes that the Assets External Target is called "Assets", and that the pipeline will place in the subdirectory "output" all the resources that must be packaged into the final application bundle.

All this can seem complex but requires little time to be properly built; having an asset pipeline integrated with your normal builds will speed up a lot your development, allowing your artists to commit new assets and immediately see the results at the next build without further coordination.