Arcweave

Arcweave is an easy to use and collaborative tool for creating narratives and dialogues that can be used in games. This can be suitable if your game lets the players talk with NPCs (non-player-characters). You can create the game's story and dialogue in Arcweave and export it to be used with Hology Engine.

Try the example project in your browser

You can access the example project's code at https://github.com/hologyengine/arcweave-tutorial to see how it can be integrated in your Hology Engine game.

Exporting

To use your Arcweave boards, you need to export it as a JSON file. In the Arcweave editor, click the Share & Export button on the top right. Select the JSON export and click export.

Using the Arcweave data

Place the exported JSON file in your code base in the src folder and rename the file to arcweave.json.

Install the Hology Arcweave library by running the following command in your terminal.

npm i @hology/arcweave

Import the file and create an ArcweaveStory instance.

import { ArcweaveStory } from '@hology/arcweave'
import awProject from './arcweave.json'

const story = new ArcweaveStory(awProject)

With the story, you can get the current element, get options for what to respond or what decision to make in dialogue and to select which option to use. The story maintains the state of which elements have been visited and global variables that are updated based on selected options.

Connecting dialogue to actors

Arcweave lets you define elements and connections between them to represent the story and paths. In an open world where you as a player have the freedom to walk up to any character to start the conversation, we need a way to trigger the game to display the correct dialogue by selecting the right element in the story. In Arcweave, you can create components that could represent a character. We can use this to connect elements to what the character is saying. We then need to know which element should be the starting point of the dialogue.

We will follow the practices used in the Game Engine Example project that comes with Arcweave. To specify the starting point of the dialogue, we add an attribute to the element with name "tag" and value "dialogue_start". To connect a character component to an actor, we define an attribute on the component with name "obj_id" and a value that will be used in the game as well. In the example, we use the name of the character in all uppercase characters.

In your Hology Engine project's code base, we will create an actor component with a parameter for the obj id so that we can specify that an instance of that actor refers to a character in the Arcweave project.

import { ActorComponent, Component, Parameter } from "@hology/core/gameplay";

@Component()
class DialogueStartComponent extends ActorComponent {

  @Parameter()
  public objId: string

}

export { DialogueStartComponent }

We will then add a few more things. A trigger volume and a dialogue service that will manage the story. In the code below, you can see how we pass the objId property to the startDialogue method on the dialogue service when the character actor starts overlapping the trigger volume. This will be used to find the element that is the starting point of the dialogue.

import { ActorComponent, attach, Component, inject, Parameter } from "@hology/core/gameplay";
import { TriggerVolumeComponent } from "@hology/core/gameplay/actors";
import Character from "../actors/character";
import { DialogueService } from "../services/dialogue-service";


@Component()
class DialogueStartComponent extends ActorComponent {

  @Parameter()
  public objId: string

  private triggerVolume = attach(TriggerVolumeComponent)
  private dialogueService = inject(DialogueService)

  onBeginPlay(): void {
    this.triggerVolume.onBeginOverlapWithActorType(Character).subscribe(() => {
      this.dialogueService.startDialogue(this.objId)
    })

    this.triggerVolume.onEndOverlapWithActorType(Character).subscribe(() => {
      this.dialogueService.endDialogue()
    })
  }

}

export { DialogueStartComponent }

For this example, we create a dialogue service to act as an interface for actors to manage the story and to expose it to the user interface. In the code below, you can see how we find the starting element of the story using the passed in objId and set the current element in the story.

import { Service } from "@hology/core/gameplay";
import { signal } from "@preact/signals-react"
import arcweaveProject from '../arcweave.json'
import { StoryOption, ArcweaveStory } from "@hology/arcweave";

export type DialogueElement = {
  speakerName?: string
  content: string
  options: StoryOption[]
  end: boolean
}

@Service()
class DialogueService {
  readonly activeDialogue = signal<DialogueElement|null>(null)
  readonly story = new ArcweaveStory(arcweaveProject)

  startDialogue(objId: string) {
    const characterId = this.story.findComponentId({attribute: [{name: 'obj_id', value: objId}]})
    if (characterId == null) {
      console.error(`Could not find character with obj_id ${objId}`)
      return
    }
    const startElementId = this.story.findElementId({attribute: [{name: 'tag', value: 'dialogue_start'}], componentId: characterId})
    if (startElementId == null) {
      console.error(`Could not find dialogue start for character with obj_id ${objId}`)
      return
    }
    this.story.setCurrentElement(startElementId)
    this.updateActiveDialogue()
  }

  endDialogue() {
    this.activeDialogue.value = null
  }

  selectOption(path: StoryOption) {
    this.story.selectOption(path)
    this.updateActiveDialogue()
  }

  private updateActiveDialogue() {
    const element = this.story.getCurrentElement()
    this.activeDialogue.value = {
      speakerName: element.components.find(c => c.attributes['obj_id'] != null)?.name,
      content: element.content,
      options: element.options,
      end: element.attributes['tag'] === 'dialogue_end' || element.options.length === 0
    }
  }
}

export { DialogueService }

Automatic refresh

Instead of exporting the Arcweave project as a JSON file and placing it in your Hology project every time you want to make test changes, we can use Arcweave's API to fetch the data. This is can be accomplished using a plugin to the build system.

Configuring the plugin

To start using the plugin, you first need to add a plugin to the Vite build configuration used in default Hology projects.

In the file vite.config.ts, replace it with the following code. What we add here is the import of the Vite plugin on line 4 and on line 11-14, we configure the plugin with your API key and project hash. See sections further down for how to find your API key and project hash.

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import hologyBuild from '@hology/vite-plugin'
import arcweave from '@hology/arcweave/vite-plugin'

export default defineConfig({
  esbuild: {
    target: "es2020",
  },
  plugins: [
    arcweave({
      apiKey: 'YOUR_ARCWEAVE_API_KEY', 
      projectHash: 'YOUR_PROJECT_HASH'
    }),
    hologyBuild(),
    react({
      babel: {
        plugins: [
          ["@babel/plugin-proposal-decorators", { version: "2023-11" }],
          ["module:@preact/signals-react-transform"],
        ],
      },
    }),
  ],
})

API Key

To get the API key, you need a team workspace in Arcweave. Then in their user interface, you can navigate to the API section and create a new API key.

Project hash

The project hash is a unique identifier for the project that you want to use in your game. To get the project hash, you need to open the project in Arcweave, and find the hash in the URL.

https://arcweave.com/app/project/PROJECT_HASH?board=...

Importing the virtual arcweave module

Instead of importing the json file from your source code, just replace it with an import form 'virtual:arcweave' . This will make the build system resolve the Arcweave project data via the Arcweave API every time you build or refresh in dev mode.

//import arcweaveProject from '../arcweave.json'
import arcweaveProject from 'virtual:arcweave'

Next steps

To learn more about how to integrate Arcweave into your project, take a look at the example project on Github.

Last updated