Skip to main content

Assets: Runtime Resources Loader

A runtime resource gallery. The project bundles three images and two JSON files under Assets/resources, then loads them from script with APJS.Resources. Tapping the preview cycles the active texture, while the HUD lists the resource paths discovered by getAllPaths() and shows the loadAll("gallery/aurora") bundle count for resources that share the same base path.

Runtime Resources Loader demo preview

What you'll build

  • A 2D gallery card with one large image, three swatches, a status label, and a live resource-path readout.
  • Runtime assets under Assets/resources/gallery/ and Assets/resources/data/, including .png textures and .userjson metadata.
  • A RuntimeResourcesLoader script that uses APJS.Resources.getAllPaths(), exist(), load(), and loadAll().
  • A JsonAsset metadata read through jsonAsset.json, used to name each card and describe what happened.
  • A RecordStart reset so recordings always begin from the first card.

Open the demo

↓ runtime-resources-loader.zip

Unzip and open in Effect House (5.9.0+). The opening scene contains:

  • Camera - default 3D perspective camera, untouched.
  • 2D Camera - auto-created when the first 2D object was added.
  • TitleText - "Runtime Resources" at the top of the UI.
  • HeroImage - the large Screen Image updated by Resources.load().
  • SwatchAurora / SwatchCitrus / SwatchOcean - three small Screen Images updated with the loaded textures.
  • StatusText - the current card title, resource path, and JSON note.
  • PathListText - the paths returned by Resources.getAllPaths(), plus the loadAll("gallery/aurora") result count.
  • GameController - empty SceneObject hosting RuntimeResourcesLoader.

The project includes these bundled files:

Assets/resources/gallery/aurora.png
Assets/resources/gallery/aurora.userjson
Assets/resources/gallery/citrus.png
Assets/resources/gallery/ocean.png
Assets/resources/data/resource_cards.userjson

aurora.png and aurora.userjson intentionally share the same base path. That makes APJS.Resources.loadAll("gallery/aurora") return both matching assets, ordered by resource-type priority.

Read the script

RuntimeResourcesLoader.ts

@component()
export class RuntimeResourcesLoader extends APJS.BasicScriptComponent {
@serializeProperty heroImage!: APJS.SceneObject;
@serializeProperty swatches: APJS.SceneObject[] = [];
@serializeProperty statusText!: APJS.SceneObject;
@serializeProperty pathListText!: APJS.SceneObject;

private readonly imagePaths: string[] = [
"gallery/aurora.png",
"gallery/citrus.png",
"gallery/ocean.png",
];

private hero!: APJS.Image;
private status!: APJS.Text;
private paths!: APJS.Text;
private swatchImages: APJS.Image[] = [];
private textures: APJS.Texture[] = [];
private titles: string[] = ["Aurora Panel", "Citrus Panel", "Ocean Panel"];
private notes: string[] = ["", "", ""];
private activeIndex: number = 0;
private inited: boolean = false;
private touchCallback!: (event: APJS.IEvent) => void;
private recordStartCallback!: (event: APJS.IEvent) => void;

onUpdate(_dt: number): void {
if (this.inited) return;
if (!this.heroImage || !this.statusText || !this.pathListText || this.swatches.length < 3) return;

this.hero = this.heroImage.getComponent("Image") as APJS.Image;
this.status = this.statusText.getComponent("Text") as APJS.Text;
this.paths = this.pathListText.getComponent("Text") as APJS.Text;
this.swatchImages = [];

for (const obj of this.swatches) {
const image = obj.getComponent("Image") as APJS.Image;
if (image) this.swatchImages.push(image);
}

const cards = this.loadCards();
for (let i = 0; i < cards.length && i < this.titles.length; i++) {
this.titles[i] = cards[i].title || this.titles[i];
this.notes[i] = cards[i].note || "";
}

this.textures = [];
for (const resourcePath of this.imagePaths) {
if (!APJS.Resources.exist(resourcePath)) {
console.error("[RuntimeResourcesLoader] missing resource " + resourcePath);
continue;
}
const texture = APJS.Resources.load(resourcePath) as APJS.Texture;
if (texture) this.textures.push(texture);
}

const allPaths = APJS.Resources.getAllPaths();
const auroraBundle = APJS.Resources.loadAll("gallery/aurora");
this.paths.text =
"Bundled paths:\n" +
allPaths.slice(0, 6).join("\n") +
"\nloadAll('gallery/aurora') -> " + auroraBundle.length + " assets";

this.touchCallback = (event: APJS.IEvent) => {
const touch = event.args[0] as APJS.TouchData;
if (touch.phase !== APJS.TouchPhase.Began) return;
this.showCard((this.activeIndex + 1) % this.textures.length);
};
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.Touch, this.touchCallback, this);

this.recordStartCallback = (_event: APJS.IEvent) => {
this.showCard(0);
};
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.RecordStart, this.recordStartCallback, this);

this.showCard(0);
this.inited = true;
console.log("[RuntimeResourcesLoader] ready, paths=" + allPaths.length + ", textures=" + this.textures.length);
}

onDestroy(): void {
const emitter = APJS.EventManager.getGlobalEmitter();
if (this.touchCallback) emitter.off(APJS.EventType.Touch, this.touchCallback, this);
if (this.recordStartCallback) emitter.off(APJS.EventType.RecordStart, this.recordStartCallback, this);
}

private loadCards(): any[] {
const metadataPath = "data/resource_cards.userjson";
if (!APJS.Resources.exist(metadataPath)) {
return [];
}
const jsonAsset = APJS.Resources.load(metadataPath) as APJS.JsonAsset;
const data = jsonAsset ? jsonAsset.json : undefined;
if (!data || !Array.isArray(data.cards)) {
return [];
}
return data.cards;
}

private showCard(index: number): void {
if (this.textures.length === 0) {
this.status.text = "No runtime resources were loaded";
return;
}

this.activeIndex = index;
const texture = this.textures[index];
this.hero.texture = texture;
this.hero.stretchMode = APJS.StretchMode.Fill;

for (let i = 0; i < this.swatchImages.length; i++) {
const image = this.swatchImages[i];
if (this.textures[i]) image.texture = this.textures[i];
image.stretchMode = APJS.StretchMode.Fill;
image.opacity = i === index ? 1 : 0.42;
}

this.status.text =
this.titles[index] +
" - " +
this.imagePaths[index] +
"\n" +
(this.notes[index] || "Loaded with APJS.Resources.load().") +
"\nTap the preview to cycle.";
}
}

API notes

  • APJS.Resources.getAllPaths() returns the original resource paths bundled with the effect. Use it for debugging and for validating that the package contains the files you expect.
  • APJS.Resources.exist(path) checks a path relative to Assets/resources. It accepts either a full file path or a suffix-less base path.
  • APJS.Resources.load(path) loads the first matching resource. In this tutorial it loads Texture objects for the three .png files and a JsonAsset for data/resource_cards.userjson.
  • APJS.Resources.loadAll(path) returns every matching resource. When you omit the suffix, resources that share the same base path are returned together. Here, gallery/aurora matches both aurora.png and aurora.userjson.
  • JsonAsset.json parses a .userjson file into a JS object or array. Treat the returned value as a read-only runtime copy; editing it does not write back to the project asset.

Try next

  • Add a fourth image under Assets/resources/gallery/, import it, and add a matching entry to data/resource_cards.userjson.
  • Replace the tap-anywhere cycling with three tappable swatches by hit-testing each swatch Image with APJS.TouchUtils.isScreenPointOnImage.
  • Use a suffix-less call like APJS.Resources.load("gallery/aurora") and compare it with loadAll("gallery/aurora") in the script logs.
Copyright © 2026 TikTok. All rights reserved.
About TikTokHelp CenterCareersContactLegalTerms of ServicePrivacy PolicyCookies