# Assets (/start/assets)



<Accordions>
  <Accordion title="What are assets?" id="what-is-assets">
    Assets are all files that are not code, such as images, sounds, and videos.
  </Accordion>
</Accordions>

Assets can be stored **locally** inside the project or hosted **online**. Frequently used assets — such as character sprites, backgrounds, and background music — are best kept local. Assets used only once or large optional files are better hosted online. Each approach has trade-offs:

## Local assets [#local-assets]

To save and use assets locally, you can use any folder—there are no restrictions. However, it is recommended to use the `src/assets` folder. Inside this folder, you can create subfolders to better organize your assets.

<Accordions>
  <Accordion title="What is PixiJS AssetPack?" id="what-is-pixijsassetpack">
    PixiJS AssetPack is a tool for optimizing local assets for the web. It can be used to transform, combine, and compress assets.

    You can learn more about PixiJS AssetPack on the [PixiJS website](https://pixijs.io/assetpack).
  </Accordion>
</Accordions>

PixiJS AssetPack is already pre-configured in the Pixi’VN project templates through the Vite.js integration — automating the processing and optimization of your assets so you can focus on building your project.

During development (`vite dev`) it watches the `src/assets` folder for changes and processes them on the fly. During the production build (`vite build`) it runs a full optimization pass before bundling.

The optimizations it applies include:

* **Compression** — reduces file sizes for faster loading.
* **Format conversion** — converts images to modern formats (e.g. WebP, AVIF) when supported.
* **Texture atlases** — combines multiple small images into a single sprite sheet to reduce draw calls.

Once processing is complete, it generates optimized files in `public/assets` and the `src/assets/manifest.gen.json` file, which contains the list of all asset bundles and their paths. These outputs are then used by PixiJS to load assets efficiently.

<Files>
  <Folder name="project">
    <Folder name="public">
      <Folder name="assets">
        <Folder name="audio">
          <File name="bgm_cheerful.ogg" />

          <File name="sfx_whoosh.ogg" />
        </Folder>

        <Folder name="mainmenu">
          <File name="background_main_menu.avif" />
        </Folder>

        <Folder name="start">
          <File name="bg01-hallway.avif" />
        </Folder>

        <Folder name="mc">
          <File name="mc-neutral.avif" />
        </Folder>
      </Folder>
    </Folder>

    <Folder name="src">
      <Folder name="assets">
        <Folder name="audio">
          <File name="bgm_cheerful.wav" />

          <File name="sfx_whoosh.wav" />
        </Folder>

        <Folder name="mainmenu">
          <File name="background_main_menu.png" />
        </Folder>

        <Folder name="start">
          <File name="bg01-hallway.webp" />
        </Folder>

        <Folder name="mc">
          <File name="mc-neutral.png" />
        </Folder>

        <File name="manifest.gen.json" />

        <File name="index.ts" />
      </Folder>
    </Folder>

    <File name=".assetpack.ts" />

    <File name="vite.config.ts" />
  </Folder>
</Files>

<CodeBlockTabs defaultValue="src/assets/manifest.gen.json">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="src/assets/manifest.gen.json">
      src/assets/manifest.gen.json
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value=".assetpack.ts">
      .assetpack.ts
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="vite.config.ts">
      vite.config.ts
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="src/assets/manifest.gen.json">
    ```json
    {
        "bundles": [
            {
                "name": "audio",
                "assets": [
                    {
                        "alias": "bgm_cheerful",
                        "src": "./assets/audio/bgm_cheerful.wav"
                    },
                    {
                        "alias": "sfx_whoosh",
                        "src": "./assets/audio/sfx_whoosh.wav"
                    }
                ]
            },
            {
                "name": "mainmenu",
                "assets": [
                    {
                        "alias": "background_main_menu",
                        "src": "./assets/mainmenu/background_main_menu.png"
                    }
                ]
            },
            {
                "name": "start",
                "assets": [
                    {
                        "alias": "bg01-hallway",
                        "src": "./assets/start/bg01-hallway.webp"
                    }
                ]
            },
            {
                "name": "mc",
                "assets": [
                    {
                        "alias": "mc-neutral",
                        "src": "./assets/mc/mc-neutral.png"
                    }
                ]
            }
        ]
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value=".assetpack.ts">
    ```ts
    import fs from "node:fs/promises";
    import type { AssetPackConfig, AssetPipe } from "@assetpack/core";
    import { path } from "@assetpack/core";
    import { pixiPipes } from "@assetpack/core/pixi";

    // TAURI_ENV_TARGET_TRIPLE is set by `tauri build` before running beforeBuildCommand
    const isTauri = !!process.env.TAURI_ENV_TARGET_TRIPLE;

    const manifestOutput = "src/assets/manifest.gen.json";

    /**
     * AssetPack only creates a manifest bundle for folders explicitly tagged
     * with "{m}" (see pixiManifest's `manifest` tag); everything else lands in
     * the single "default" bundle. This splits the "default" bundle into one
     * bundle per top-level folder under the assets entry, named after that
     * folder, so bundles mirror the folder structure without needing to tag
     * every folder manually. Files directly at the entry root (no folder) stay
     * in "default". Runs after the manifest has been written to disk, so it
     * re-reads and rewrites the file.
     */
    function groupBundlesByFolder(options: {
        output: string;
    }): AssetPipe<{ output: string }> {
        return {
            name: "group-bundles-by-folder",
            defaultOptions: options,
            async finish(_asset, opts) {
                const manifest = JSON.parse(
                    await fs.readFile(opts.output, "utf-8"),
                );

                const bundles = new Map<
                    string,
                    { name: string; assets: unknown[] }
                >();
                for (const bundle of manifest.bundles) {
                    if (bundle.name !== "default") {
                        bundles.set(bundle.name, bundle);
                        continue;
                    }
                    for (const asset of bundle.assets) {
                        const folder = path.dirname(asset.src[0]).split("/")[0];
                        const bundleName = folder === "." ? "default" : folder;
                        const target = bundles.get(bundleName) ?? {
                            name: bundleName,
                            assets: [],
                        };
                        target.assets.push(asset);
                        bundles.set(bundleName, target);
                    }
                }
                manifest.bundles = [...bundles.values()];

                await fs.writeFile(opts.output, JSON.stringify(manifest, null, 2));
            },
        };
    }

    /**
     * Removes file extensions from the aliases generated by the pixi manifest
     * pipe and replaces path separators with underscores (e.g.
     * "yard/yard1.png" -> "yard_yard1"). Runs after the manifest has been
     * written to disk, so it re-reads and rewrites the file.
     *
     * If two assets end up with the same normalized alias, the later one is
     * prefixed with its bundle name (e.g. "yard_yard_yard1") to keep aliases
     * unique across the whole manifest.
     */
    function normalizeAliases(options: {
        output: string;
    }): AssetPipe<{ output: string }> {
        return {
            name: "normalize-aliases",
            defaultOptions: options,
            async finish(_asset, opts) {
                const manifest = JSON.parse(
                    await fs.readFile(opts.output, "utf-8"),
                );

                const seen = new Set<string>();
                for (const bundle of manifest.bundles) {
                    for (const asset of bundle.assets) {
                        asset.alias = asset.alias.map((alias: string) => {
                            const normalized = path
                                .trimExt(alias)
                                .replaceAll("/", "_");
                            const unique = seen.has(normalized)
                                ? `${bundle.name}_${normalized}`
                                : normalized;
                            seen.add(normalized);
                            return unique;
                        });
                    }
                }

                await fs.writeFile(opts.output, JSON.stringify(manifest, null, 2));
            },
        };
    }

    const config: AssetPackConfig = {
        entry: "./src/assets",
        output: "./public/assets",
        ignore: ["**/*.ts", "**/*.js", "**/*.gen.*"],
        pipes: [
            ...pixiPipes({
                manifest: {
                    output: manifestOutput,
                    createShortcuts: true,
                },
                // For Tauri: skip @0.5x mipmaps (unused on desktop, WebView handles DPR)
                resolutions: isTauri ? { default: 1 } : undefined,
                // For Tauri: raise WebP quality — files are local so bandwidth is not a concern
                compression: isTauri
                    ? {
                          png: true,
                          jpg: true,
                          webp: { quality: 88, alphaQuality: 88 },
                      }
                    : undefined,
            }),
            groupBundlesByFolder({ output: manifestOutput }),
            normalizeAliases({ output: manifestOutput }),
        ],
    };

    export default config;
    ```
  </CodeBlockTab>

  <CodeBlockTab value="vite.config.ts">
    ```ts
    import { AssetPack } from "@assetpack/core";
    import { defineConfig, type Plugin, type ResolvedConfig } from "vite";
    import assetPackConfig from "./.assetpack.ts";

    export default defineConfig(() => ({
        plugins: [
            assetpackPlugin(),
            // ...
        ],
    }));

    function assetpackPlugin(): Plugin {
        let mode: ResolvedConfig["command"];
        let ap: AssetPack | undefined;

        return {
            name: "vite-plugin-assetpack",
            configResolved(resolvedConfig) {
                mode = resolvedConfig.command;
                if (!resolvedConfig.publicDir) return;
                if (assetPackConfig.output) return;
                const publicDir = resolvedConfig.publicDir.replace(
                    process.cwd(),
                    "",
                );
                assetPackConfig.output = `.${publicDir}/assets/`;
            },
            buildStart: async () => {
                if (mode === "serve") {
                    if (ap) return;
                    ap = new AssetPack(assetPackConfig);
                    void ap.watch();
                } else {
                    await new AssetPack(assetPackConfig).run();
                }
            },
            buildEnd: async () => {
                if (ap) {
                    await ap.stop();
                    ap = undefined;
                }
            },
        };
    }
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Assets hosting [#assets-hosting]

You can save your assets online. This is a good option if you want to save space in your project or if you want create a game playable online without the need to download it first. You can use any cloud service that allows you to upload files and generate a public URL (you need to make sure that the cloud service you are using allows *CORS requests*).

Here are some popular options for hosting your assets online:

<Accordions>
  <Accordion title="Github" id="github">
    You can use Github to host your assets. You can use the raw link of the file in your project. The link will be in the following format:

    ```text
    https://raw.githubusercontent.com/[repository]/raw/refs/heads/main/[file path]
    ```

    * **Price**: Completely free.
    * **Space limits**: No space limits, but each single file must not exceed 100 MB.
    * **Type of files**: You can upload any type of file.
    * **Traffic**: Speed is not the best.
    * **Edit assets**: You can edit the file while keeping the same URL.
  </Accordion>

  <Accordion title="Image hosting" id="image-hosting">
    Image hosting is a service that allows you to upload images. There are many sites to upload images for free, for example [imgbb](https://imgbb.com/), [imgix](https://www.imgix.com/), [imgur](https://imgur.com/). You can use the link of the image in your project.

    * **Price**: Completely free, but you can pay for more features.
    * **Space limits**: No space limits, but each single file can have a maximum size.
    * **Type of files**: You can upload only images.
    * **Traffic**: Speed is good.
    * **Edit assets**: You can't edit the file while keeping the same URL.
  </Accordion>

  <Accordion title="Cloud storage" id="Cloud-storage">
    Cloud storage is a service that allows you to upload any type of file online. There are many popular options, for example [Cloudflare R2](https://www.cloudflare.com/developer-platform/r2/), [Firebase Storage](https://firebase.google.com/pricing), [Amazon S3](https://aws.amazon.com/s3/pricing/), [Supabase](https://supabase.io/pricing), [Convex](https://www.convex.dev/pricing).

    * **Price**: Usually paid or with a free version with limits.
    * **Space limits**: Monthly cost based on space used (usually free if you do not exceed a certain threshold).
    * **Type of files**: You can upload any type of file.
    * **Traffic**: Speed is good.
    * **Edit assets**: You can edit the file while keeping the same URL.
  </Accordion>
</Accordions>

Once you have your assets ready, you need to manually define which ones your game can use and how they are grouped into bundles.

<CalloutContainer type="info">
  <CalloutDescription>
    It is strongly recommended to follow the <DynamicLink href="/start/assets-management">asset loading best practices</DynamicLink> to minimize loading times and avoid blocking the game unnecessarily.
  </CalloutDescription>
</CalloutContainer>

All Pixi’VN templates include a `src/assets/index.ts` file that you can edit to register your assets:

```ts title="src/assets/index.ts"
import generatedManifestJson from "@/assets/manifest.gen.json";
import { AUDIO_BUNDLE_NAME } from "@/constants";
import { startLabel } from "@/content/labels/start.label";
import type { FileRouteTypes } from "@/routeTree.gen";
import type { AssetsManifest } from "@drincs/pixi-vn";

export const manifest: AssetsManifest = {
    bundles: [
        ...generatedManifestJson.bundles,
        {
            name: AUDIO_BUNDLE_NAME,
            assets: [
                {
                    alias: "bgm_cheerful",
                    src: "https://raw.githubusercontent.com/user/project/refs/heads/main/audio/bgm_cheerful.wav",
                },
                {
                    alias: "sfx_whoosh",
                    src: "https://raw.githubusercontent.com/user/project/refs/heads/main/audio/sfx_whoosh.wav",
                },
            ],
        },
        // screens
        {
            // main menu
            name: "/" as FileRouteTypes["fullPaths"],
            assets: [
                {
                    alias: "background_main_menu",
                    src: "https://raw.githubusercontent.com/user/project/refs/heads/main/main-menu.png",
                },
            ],
        },
        // labels
        {
            name: startLabel.id,
            assets: [
                {
                    alias: "bg01-hallway",
                    src: "https://raw.githubusercontent.com/user/project/refs/heads/main/breakdown/bg01-hallway.webp",
                },
            ],
        },
        // characters
        {
            name: "mc",
            assets: [
                {
                    alias: "mc-neutral",
                    src: "https://raw.githubusercontent.com/user/project/refs/heads/main/characters/mc-neutral.png",
                },
            ],
        },
    ],
};
```

## Other features [#other-features]

<Accordions>
  <Accordion title="Caching assets" id="caching-assets">
    <CalloutContainer type="info">
      <CalloutTitle>
        Templates
      </CalloutTitle>

      <CalloutDescription>
        In all templates, assets loaded from external URLs are automatically cached by the service worker for offline use. This is configured in the `vite.config.ts` file using the `vite-plugin-pwa` plugin.
      </CalloutDescription>
    </CalloutContainer>

    For online games, caching external assets is crucial: it avoids re-downloading the same files on every session, reduces latency, and allows the game to work even when the connection is temporarily unavailable.

    This is handled by [VitePWA](https://vite-pwa-org.netlify.app/), which is pre-configured in the templates. You can customize the caching behavior directly in `vite.config.ts`:

    * **`CACHED_EXTERNAL_HOSTNAMES`** — list of external hostnames whose assets should be cached. Add any CDN or remote host you use.
    * **`cacheableResponse`** — defines which HTTP responses are eligible for caching (e.g. status `0` for opaque responses and `200` for successful ones).
    * **`expiration`** — controls how long cached assets are kept. By default assets are cached for 7 days (`maxAgeSeconds: 7 * 24 * 60 * 60`).

    ```ts title="vite.config.ts"
    import { defineConfig } from "vite";
    import { VitePWA } from "vite-plugin-pwa";

    /**
     * List of external hostnames whose responses should be cached by the service worker.
     * Add any CDN or remote asset host here to enable offline caching for it.
     * Examples:
     *   "cdn.jsdelivr.net"
     *   "your.cdn.domain.com"
     */
    const CACHED_EXTERNAL_HOSTNAMES: string[] = ["raw.githubusercontent.com"];

    export default defineConfig(() => ({
        plugins: [
            VitePWA({
                // ...
                workbox: {
                    runtimeCaching: [
                        {
                            urlPattern: ({ url }) =>
                                CACHED_EXTERNAL_HOSTNAMES.includes(url.hostname),
                            handler: "CacheFirst",
                            options: {
                                cacheName: "external-assets-v1",
                                cacheableResponse: {
                                    statuses: [0, 200],
                                },
                                expiration: {
                                    maxAgeSeconds: 7 * 24 * 60 * 60,
                                },
                            },
                        },
                    ],
                },
            }),
        ],
    }));
    ```
  </Accordion>
</Accordions>
