Skip to content

Commit

Permalink
Merge pull request #7 from DrJosh9000/sync-handler
Browse files Browse the repository at this point in the history
  • Loading branch information
DrJosh9000 authored May 9, 2023
2 parents 8767399 + c4a2610 commit b3f1d56
Show file tree
Hide file tree
Showing 6 changed files with 872 additions and 65 deletions.
136 changes: 77 additions & 59 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ commands to the handler.
*`visited` and `visit_count`
* ✅ Built-in functions like `dice`, `round`, and `floor` that are mentioned in the Yarn Spinner documentation.

## Usage
## Basic Usage

1. Compile your `.yarn` file. You can probably get the compiled output from a
1. Compile your `.yarn` file. You can probably get the compiled output from a
Unity project, or you can compile without using Unity with a tool like the
[Yarn Spinner Console](https://github.com/YarnSpinnerTool/YarnSpinner-Console):

Expand Down Expand Up @@ -75,7 +75,7 @@ commands to the handler.
return choice, nil
}

// ... and also the other methods.
// ... and also the other methods.
// Alternatively you can embed yarn.FakeDialogueHandler in your handler.
```

Expand All @@ -85,12 +85,12 @@ commands to the handler.

```go
package main

import "github.com/DrJosh9000/yarn"

func main() {
// Load the files (error handling omitted for brevity):
program, stringTable, _ := yarn.LoadFiles("Example.yarn.yarnc", "Example.yarn.csv", "en-AU")
program, stringTable, _ := yarn.LoadFiles("Example.yarn.yarnc", "en-AU")

// Set up your DialogueHandler and the VirtualMachine:
myHandler := &MyHandler{
Expand All @@ -99,9 +99,9 @@ commands to the handler.
vm := &yarn.VirtualMachine{
Program: program,
Handler: myHandler,
Vars: make(yarn.MapVariableStorage), // or your own VariableStorage implementation
Vars: make(yarn.MapVariableStorage), // or your own VariableStorage implementation
FuncMap: yarn.FuncMap{ // this is optional
"last_value": func(x ...interface{}) interface{} {
"last_value": func(x ...any) any {
return x[len(x)-1]
},
// or your own custom functions!
Expand All @@ -115,83 +115,101 @@ commands to the handler.

See `cmd/yarnrunner.go` for a complete example.

## Usage notes
## Async usage

Note that using an earlier Yarn Spinner compiler will result in some unusual
behaviour when compiling Yarn files with newer features. For example, with v1.0
`<<jump ...>>` may be compiled as a command. Your implementation of `Command`
may implement `jump` by calling the `SetNode` VM method.
To avoid the VM delivering the lines, options, and commands all at once,
your `DialogueHandler` implementation is allowed to block execution of the VM
goroutine - for example, using a channel operation.

If you need the tags for a node, you can read these from the `Node` protobuf
message directly. Source text of a `rawText` node can be looked up manually:
However, in a typical game, each line or option would be associated with two
distinct operations: showing the line/option to the player, and hiding it later
on in response to user input.

```go
prog, st, _ := yarn.LoadFiles("testdata/Example.yarn.yarnc", "testdata/Example.yarn.csv", "en")
node := prog.Nodes["LearnMore"]
// Tags for the LearnMore node:
fmt.Println(node.Tags)
// Source text string ID:
fmt.Println(node.SourceTextStringID)
// Source text is in the string table:
fmt.Println(st.Table[node.SourceTextStringID].Text)
To make this easier, `AsyncAdapter` can handle blocking the VM for you.

```mermaid
sequenceDiagram
yarn.VirtualMachine->>+yarn.AsyncAdapter: Line
yarn.AsyncAdapter->>+myHandler: Line
myHandler->>-gameEngine: showDialogue
Note right of myHandler: (time passes)
gameEngine->>+myHandler: Update
myHandler->>gameEngine: hideDialogue
myHandler->>-yarn.AsyncAdapter: Go
yarn.AsyncAdapter-->>-yarn.VirtualMachine: (return)
```

In a typical game, `vm.Run` would happen in a separate goroutine. To avoid the
VM delivering all the lines, options, and commands at once, your
`DialogueHandler` implementation is allowed to block execution of the VM
goroutine - for example, using a channel operation:
Use
`AsyncAdapter` as the `VirtualMachine.Handler`, and create the `AsyncAdapter`
with an `AsyncDialogueHandler`:

```go
// MyHandler should now implement yarn.AsyncDialogueHandler.
type MyHandler struct {
stringTable *yarn.StringTable

dialogueDisplay Component

// next is used to block Line from returning until the player is ready for
// more tasty, tasty content.
next chan struct{}

// waiting tracks whether the game is waiting for player input.
// It is guarded by a mutex since it is changed by two different
// goroutines.
waitingMu sync.Mutex
waiting bool
}

func (m *MyHandler) setWaiting(w bool) {
m.waitingMu.Lock()
m.waiting = w
m.waitingMu.Unlock()
// Maintain a reference to the AsyncAdapter in order to call Go on it
// in response to user input.
// (It doesn't have to be stored in the handler, there are probably better
// places in a real project. This is just an example.)
asyncAdapter *yarn.AsyncAdapter
}

// Line is called from the goroutine running VirtualMachine.Run.
func (m *MyHandler) Line(line yarn.Line) error {
// Line is called by AsyncAdapter from the goroutine running VirtualMachine.Run.
// The AsyncAdapter pauses the VM.
func (m *MyHandler) Line(line yarn.Line) {
text, _ := m.stringTable.Render(line)
m.dialogueDisplay.Show(text)

// Go into waiting-for-player-input state
m.setWaiting(true)

// Recieve on m.next, which blocks until another goroutine sends on it.
<-m.next
return nil
}

// Update is called on every tick by the game engine, which is a separate
// goroutine to the one the virtual machine is running in.
// goroutine to the one the Yarn virtual machine is running in.
func (m *MyHandler) Update() error {
//...
if m.waiting && inpututil.IsKeyJustPressed(ebiten.KeyEnter) {

if m.dialogueDisplay.Visible() && inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
// Hide the dialogue display.
m.dialogueDisplay.Hide()
// No longer waiting for player input.
m.setWaiting(false)
// Send on m.next, which unblocks the call to Line.
// Do this after setting m.waiting to false.
m.next <- struct{}{}

// Calling AsyncAdapter.Go un-pauses the VM.
m.asyncAdapter.Go()
}
//...
}

// --- Setup ---

myHandler := &MyHandler{}
myHandler.asyncAdapter = yarn.NewAsyncAdapter(myHandler)

vm := &yarn.VirtualMachine{
Program: program,
Handler: myHandler.asyncAdapter,
...
}
```

## Usage notes

Note that using an earlier Yarn Spinner compiler will result in some unusual
behaviour when compiling Yarn files with newer features. For example, with v1.0
`<<jump ...>>` may be compiled as a command. Your implementation of `Command`
may implement `jump` by calling the `SetNode` VM method.

If you need the tags for a node, you can read these from the `Node` protobuf
message directly. Source text of a `rawText` node can be looked up manually:

```go
prog, st, _ := yarn.LoadFiles("testdata/Example.yarn.yarnc", "en")
node := prog.Nodes["LearnMore"]
// Tags for the LearnMore node:
fmt.Println(node.Tags)
// Source text string ID:
fmt.Println(node.SourceTextStringID)
// Source text is in the string table:
fmt.Println(st.Table[node.SourceTextStringID].Text)
```

## Licence
Expand Down
Loading

0 comments on commit b3f1d56

Please sign in to comment.