Skip to content
This repository was archived by the owner on Oct 26, 2023. It is now read-only.

Commit e0a53ea

Browse files
committed
Initial commit.
0 parents  commit e0a53ea

File tree

5 files changed

+369
-0
lines changed

5 files changed

+369
-0
lines changed

example/main.go

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/solarlune/gocoro"
8+
)
9+
10+
var gameFrame int
11+
var progress = -1
12+
var maxProgress = 20
13+
14+
// Here's the coroutine function to run in our Coroutine. It takes an execution object that
15+
// allows us to do some easy coroutine manipulation (yield, wait until something happens, etc).
16+
// If we want to pause execution, we can call Execution.Yield(). If we want to end execution early,
17+
// just return from the function like usual. If you want to end execution from *outside* this function
18+
// (i.e. with Coroutine.Kill()), then you can pick up on that through Execution.Killed() and return early.
19+
func coroutineFunction(exe *gocoro.Execution) {
20+
21+
fmt.Printf("\nFrame #%d: Let's start the script and wait three seconds.\n", gameFrame)
22+
23+
exe.Wait(time.Second * 3)
24+
25+
fmt.Printf("\nFrame #%d: Excellent! Let's wait 35 ticks this time.\n", gameFrame)
26+
27+
exe.WaitTicks(35)
28+
29+
fmt.Printf("\nFrame #%d: Let's fill this progress bar:\n", gameFrame)
30+
31+
exe.Wait(time.Second * 2)
32+
33+
fmt.Println("")
34+
35+
for progress < maxProgress-1 {
36+
progress++
37+
exe.Yield()
38+
}
39+
40+
progress = -1
41+
42+
fmt.Printf("\nFrame #%d: Excellent, again!\n", gameFrame)
43+
44+
exe.Wait(time.Second)
45+
46+
fmt.Printf("\nFrame #%d: OK, script's over, let's go home!\n", gameFrame)
47+
48+
exe.Wait(time.Second)
49+
50+
}
51+
52+
func main() {
53+
54+
// Create a new coroutine.
55+
co := gocoro.NewCoroutine()
56+
57+
// Run the script. It actually will technically only start when we call Coroutine.Update() below.
58+
co.Run(coroutineFunction)
59+
60+
// co.Running is thread-safe
61+
for co.Running() {
62+
63+
// Update the script. This function call will run the coroutine thread for as long as is necessary,
64+
// until it either yields or finishes.
65+
co.Update()
66+
67+
fmt.Print(".")
68+
69+
// Draw the progress bar when it's time to do so
70+
if progress >= 0 {
71+
pro := "["
72+
73+
for i := 0; i < maxProgress; i++ {
74+
if i > progress {
75+
pro += " "
76+
} else {
77+
pro += "█"
78+
}
79+
}
80+
pro += "]"
81+
82+
fmt.Println(pro)
83+
}
84+
85+
gameFrame++
86+
87+
time.Sleep(time.Millisecond * 100)
88+
89+
}
90+
91+
fmt.Println("\n\nCoroutine finished!")
92+
93+
time.Sleep(time.Second * 1)
94+
95+
}

go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/solarlune/gocoro
2+
3+
go 1.19

gocoro.go

+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package gocoro
2+
3+
import (
4+
"errors"
5+
"sync/atomic"
6+
"time"
7+
)
8+
9+
// Coroutine represents a coroutine that executes alternately with the main / calling
10+
// thread.
11+
type Coroutine struct {
12+
routine func(*Execution)
13+
running *atomic.Bool
14+
yield chan bool
15+
execute chan bool
16+
Execution *Execution
17+
}
18+
19+
// NewCoroutine creates and returns a new Coroutine instance.
20+
func NewCoroutine() Coroutine {
21+
co := Coroutine{
22+
yield: make(chan bool),
23+
execute: make(chan bool),
24+
running: &atomic.Bool{},
25+
}
26+
co.Execution = &Execution{coroutine: &co}
27+
return co
28+
}
29+
30+
// Run runs the given coroutine function. Note that the function takes the Coroutine
31+
// as an argument to allow for a variety of methods of defining this function (as a
32+
// literal in the Run() function call, as a pointer to a pre-defined function, etc).
33+
// Run will return an error if the coroutine is already running.
34+
func (co *Coroutine) Run(coroutineFunc func(exe *Execution)) error {
35+
36+
if !co.running.Load() {
37+
38+
co.running.Store(true)
39+
40+
co.routine = coroutineFunc
41+
42+
go func() {
43+
// Send something on execute first so the script doesn't update until we
44+
// call Coroutine.Update() the first time.
45+
co.execute <- true
46+
co.routine(co.Execution)
47+
wasRunning := co.running.Load()
48+
co.running.Store(false)
49+
// If the coroutine wasn't running anymore, then we shouldn't push anything to yield to unblock the coroutine at the end
50+
if wasRunning {
51+
co.yield <- true
52+
}
53+
}()
54+
55+
return nil
56+
57+
} else {
58+
return errors.New("Coroutine is already running")
59+
}
60+
61+
}
62+
63+
// Running returns whether the Coroutine is running or not.
64+
func (co *Coroutine) Running() bool {
65+
return co.running.Load()
66+
}
67+
68+
// Update waits for the Coroutine to pause, either as a yield or when the Coroutine is finished.
69+
func (co *Coroutine) Update() {
70+
if co.running.Load() {
71+
<-co.execute // Wait to pull from the execute channel, indicating the coroutine can run
72+
<-co.yield // Wait to pull from the yield channel, indicating the coroutine has paused / finished
73+
}
74+
}
75+
76+
// Stop stops running the Coroutine and allows the CoroutineExecution to pick up on it to end gracefully.
77+
// This does not kill the coroutine, which runs in a goroutine - you'll need to detect this and
78+
// end the coroutine yourself.
79+
func (co *Coroutine) Stop() {
80+
wasRunning := co.running.Load()
81+
co.running.Store(false)
82+
if wasRunning {
83+
<-co.execute // Pull from the execute channel so the coroutine can get out of the yield and realize it's borked
84+
}
85+
}
86+
87+
var ErrorCoroutineStopped = errors.New("Coroutine requested to be stopped")
88+
89+
// Execution represents a means to easily and simply manipulate coroutine execution from your running coroutine function.
90+
type Execution struct {
91+
coroutine *Coroutine
92+
}
93+
94+
// Yield yields execution in the coroutine function, allowing the main / calling thread to continue.
95+
// The coroutine will pick up from this point when Coroutine.Update() is called again.
96+
// If the Coroutine has exited already, then this will immediately return with ErrorCoroutineStopped.
97+
func (exe *Execution) Yield() error {
98+
99+
if !exe.coroutine.Running() {
100+
return ErrorCoroutineStopped
101+
}
102+
103+
exe.coroutine.yield <- true // Yield; we're done
104+
exe.coroutine.execute <- true // Put something in the execute channel when we're ready to pick back up if we're not done
105+
106+
return nil
107+
108+
}
109+
110+
// Stopped returns true if the coroutine was requested to be stopped. You can check this in your coroutine to exit early and
111+
// clean up the coroutine as desired.
112+
func (exe *Execution) Stopped() bool {
113+
return !exe.coroutine.Running()
114+
}
115+
116+
// Wait waits the specified duration time, yielding execution in the Coroutine if the time has yet to elapse.
117+
// Note that this function only checks the time in increments of however long the calling thread takes between calling Coroutine.Update().
118+
// So, for example, if Coroutine.Update() is run, say, once every 20 milliseconds, then that's the fidelity of your waiting duration.
119+
// If the Coroutine has exited already, then this will immediately return with ErrorCoroutineStopped.
120+
func (exe *Execution) Wait(duration time.Duration) error {
121+
start := time.Now()
122+
for {
123+
124+
if time.Since(start) >= duration {
125+
return nil
126+
} else {
127+
if err := exe.Yield(); err != nil {
128+
return err
129+
}
130+
}
131+
}
132+
}
133+
134+
// WaitTicks waits the specified number of ticks, yielding execution if the number of ticks have yet to elapse. A tick is defined by one instance
135+
// of Coroutine.Update() being called.
136+
// If the Coroutine has exited already, then this will immediately return with ErrorCoroutineStopped.
137+
func (exe *Execution) WaitTicks(tickCount int) error {
138+
for {
139+
140+
if tickCount == 0 {
141+
return nil
142+
} else {
143+
tickCount--
144+
if err := exe.Yield(); err != nil {
145+
return err
146+
}
147+
}
148+
149+
}
150+
151+
}
152+
153+
// WaitUntil pauses the Coroutine until the provided Completer's Done() function returns true.
154+
// If the Coroutine has exited already, then this will immediately return with ErrorCoroutineStopped.
155+
func (exe *Execution) WaitUntil(completer Completer) error {
156+
157+
for {
158+
159+
if completer.Done() {
160+
return nil
161+
} else {
162+
if err := exe.Yield(); err != nil {
163+
return err
164+
}
165+
}
166+
}
167+
168+
}
169+
170+
// Do pauses the running Coroutine until the provided function returns true.
171+
// If the Coroutine has exited already, then this will immediately return with ErrorCoroutineStopped.
172+
func (exe *Execution) Do(doFunc func() bool) error {
173+
174+
for {
175+
if doFunc() {
176+
return nil
177+
} else {
178+
if err := exe.Yield(); err != nil {
179+
return err
180+
}
181+
}
182+
}
183+
184+
}
185+
186+
// Completer provides an interface of an object that can be used to pause a Coroutine until it is completed.
187+
// If the Completer's Done() function returns true, then the Coroutine will advance.
188+
type Completer interface {
189+
Done() bool
190+
}

license

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 SolarLune
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

readme.md

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# gocoro 🏃‍♂️ ➡️ 🧍‍♂️
2+
3+
gocoro is a package for basic coroutines for Go. The primary reason I made this was for creating cutscenes for gamedev with Go.
4+
5+
## What's a coroutine?
6+
7+
Normally, a coroutine is just a function that you can pause and resume execution on at will (or possibly even freely jump around at will). Coroutines could have a variety of uses, but are particularly good for scripting cutscenes in games because cutscenes frequently pause and pick up execution (for example, when waiting for some amount of time, displaying a message window, or animating or moving characters to another location).
8+
9+
## Why did you make this package?
10+
11+
Because coroutines are cool and useful, and Go has almost everything necessary for coroutines (like jumping between labels, pausing / blocking execution, etc). Combine that with Go being a fundamentally good, simple, and not too verbose programming language, and it seems like a good solution to this problem, rather than implementing your own scripting language or using a slice of cutscene actions or something.
12+
13+
## How do I use it?
14+
15+
`go get github.com/solarlune/gocoro`
16+
17+
## Example
18+
19+
```go
20+
21+
func script(exe gocoro.Execution) {
22+
23+
// Use the Execution object to pause and wait for three seconds.
24+
exe.Wait(time.Second * 3)
25+
26+
fmt.Println("Three seconds have elapsed!")
27+
28+
}
29+
30+
func main() {
31+
32+
// Create a new Coroutine.
33+
co := gocoro.NewCoroutine()
34+
35+
// Run the script, which is just an ordinary function pointer that takes
36+
// an execution object, which is used to control coroutine execution.
37+
co.Run(script)
38+
39+
for co.Running() {
40+
41+
// While the coroutine runs, we call Coroutine.Update(). This allows
42+
// the coroutine to execute, but also gives control back to the main
43+
// thread when it's yielding so we can do other stuff.
44+
co.Update()
45+
46+
}
47+
48+
// We're done with the coroutine!
49+
50+
}
51+
52+
```
53+
54+
## What's a gocoro.Coroutine?
55+
56+
Internally, a `*gocoro.Coroutine` is just a goroutine running a customizeable function. This means that it executes on another thread. However, gocoro uses a channel to alternately block execution on the calling thread or the coroutine thread, allowing you to pause execution and resume it at will. Because the operating thread alternates between the two, there's no opportunity for race conditions if they both touch the same data.
57+
58+
## Anything else?
59+
60+
Not really, that's it. Peace~

0 commit comments

Comments
 (0)