reorganize module
This commit is contained in:
232
_archive/rhai_engine/rhaibook/patterns/events.md
Normal file
232
_archive/rhai_engine/rhaibook/patterns/events.md
Normal file
@@ -0,0 +1,232 @@
|
||||
Scriptable Event Handler with State
|
||||
===================================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish tip "IMPORTANT PATTERN"
|
||||
|
||||
In many usage scenarios, a scripting engine is used to provide flexibility in event handling.
|
||||
|
||||
That means to execute certain **actions** in response to certain **_events_** that occur at run-time,
|
||||
and scripts are used to provide flexibility for coding those actions.
|
||||
|
||||
You'd be surprised how many applications fit under this pattern – they are all essentially
|
||||
event handling systems.
|
||||
```
|
||||
|
||||
```admonish example "Examples"
|
||||
|
||||
Because of the importance of this pattern, runnable examples are included.
|
||||
|
||||
See the [_Examples_](../start/examples/rust.md) section for details.
|
||||
```
|
||||
|
||||
```admonish info "Usage scenario"
|
||||
|
||||
* A system sends _events_ that must be handled.
|
||||
|
||||
* Flexibility in event handling must be provided, through user-side scripting.
|
||||
|
||||
* State must be kept between invocations of event handlers.
|
||||
|
||||
* State may be provided by the system or the user, or both.
|
||||
|
||||
* Default implementations of event handlers can be provided.
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* An _event handler_ object is declared that holds the following items:
|
||||
* [`Engine`] with registered functions serving as an API,
|
||||
* [`AST`] of the user script,
|
||||
* [`Scope`] containing system-provided default state.
|
||||
|
||||
* User-provided state is initialized by a [function] called via [`Engine::call_fn_with_options`][`call_fn`].
|
||||
|
||||
* Upon an event, the appropriate event handler [function] in the script is called via
|
||||
[`Engine::call_fn`][`call_fn`].
|
||||
|
||||
* Optionally, trap the `EvalAltResult::ErrorFunctionNotFound` error to provide a default implementation.
|
||||
```
|
||||
|
||||
|
||||
Basic Infrastructure
|
||||
--------------------
|
||||
|
||||
### Declare handler object
|
||||
|
||||
In most cases, it would be simpler to store an [`Engine`] instance together with the handler object
|
||||
because it only requires registering all API functions only once.
|
||||
|
||||
In rare cases where handlers are created and destroyed in a tight loop, a new [`Engine`] instance
|
||||
can be created for each event. See [_One Engine Instance Per Call_](parallel.md) for more details.
|
||||
|
||||
```rust
|
||||
use rhai::{Engine, Scope, AST};
|
||||
|
||||
// Event handler
|
||||
struct Handler {
|
||||
// Scripting engine
|
||||
pub engine: Engine,
|
||||
// Use a custom 'Scope' to keep stored state
|
||||
pub scope: Scope<'static>,
|
||||
// Program script
|
||||
pub ast: AST
|
||||
}
|
||||
```
|
||||
|
||||
### Register API for custom types
|
||||
|
||||
[Custom types] are often used to hold state. The easiest way to register an entire API is via a [plugin module].
|
||||
|
||||
```rust
|
||||
use rhai::plugin::*;
|
||||
|
||||
// A custom type to a hold state value.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub struct TestStruct {
|
||||
data: i64
|
||||
}
|
||||
|
||||
// Plugin module containing API to TestStruct
|
||||
#[export_module]
|
||||
mod test_struct_api {
|
||||
#[rhai_fn(global)]
|
||||
pub fn new_state(value: i64) -> TestStruct {
|
||||
TestStruct { data: value }
|
||||
}
|
||||
#[rhai_fn(global)]
|
||||
pub fn func1(obj: &mut TestStruct) -> bool {
|
||||
:
|
||||
}
|
||||
#[rhai_fn(global)]
|
||||
pub fn func2(obj: &mut TestStruct) -> i64 {
|
||||
:
|
||||
}
|
||||
pub fn process(data: i64) -> i64 {
|
||||
:
|
||||
}
|
||||
#[rhai_fn(get = "value", pure)]
|
||||
pub fn get_value(obj: &mut TestStruct) -> i64 {
|
||||
obj.data
|
||||
}
|
||||
#[rhai_fn(set = "value")]
|
||||
pub fn set_value(obj: &mut TestStruct, value: i64) {
|
||||
obj.data = value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Initialize handler object
|
||||
|
||||
Steps to initialize the event handler:
|
||||
|
||||
1. Register an API with the [`Engine`],
|
||||
2. Create a custom [`Scope`] to serve as the stored state,
|
||||
3. Add default state [variables] into the custom [`Scope`],
|
||||
4. Optionally, call an initiation [function] to create new state [variables];
|
||||
`Engine::call_fn_with_options` is used instead of `Engine::call_fn` so that [variables] created
|
||||
inside the [function] will not be removed from the custom [`Scope`] upon exit,
|
||||
5. Get the handler script and [compile][`AST`] it,
|
||||
6. Store the compiled [`AST`] for future evaluations,
|
||||
7. Run the [`AST`] to initialize event handler state [variables].
|
||||
|
||||
```rust
|
||||
impl Handler {
|
||||
// Create a new 'Handler'.
|
||||
pub fn new(path: impl Into<PathBuf>) -> Self {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register custom types and APIs
|
||||
engine.register_type_with_name::<TestStruct>("TestStruct")
|
||||
.register_global_module(exported_module!(test_struct_api).into());
|
||||
|
||||
// Create a custom 'Scope' to hold state
|
||||
let mut scope = Scope::new();
|
||||
|
||||
// Add any system-provided state into the custom 'Scope'.
|
||||
// Constants can be used to optimize the script.
|
||||
scope.push_constant("MY_CONSTANT", 42_i64);
|
||||
|
||||
:
|
||||
:
|
||||
// Initialize state variables
|
||||
:
|
||||
:
|
||||
|
||||
// Compile the handler script.
|
||||
// In a real application you'd be handling errors...
|
||||
let ast = engine.compile_file_with_scope(&mut scope, path).unwrap();
|
||||
|
||||
// The event handler is essentially these three items:
|
||||
Self { engine, scope, ast }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Hook up events
|
||||
|
||||
There is usually an interface or trait that gets called when an event comes from the system.
|
||||
|
||||
Mapping an event from the system into a scripted handler is straight-forward, via `Engine::call_fn`.
|
||||
|
||||
```rust
|
||||
impl Handler {
|
||||
// Say there are three events: 'start', 'end', 'update'.
|
||||
// In a real application you'd be handling errors...
|
||||
pub fn on_event(&mut self, event_name: &str, event_data: i64) -> Dynamic {
|
||||
let engine = &self.engine;
|
||||
let scope = &mut self.scope;
|
||||
let ast = &self.ast;
|
||||
|
||||
match event_name {
|
||||
// The 'start' event maps to function 'start'.
|
||||
// In a real application you'd be handling errors...
|
||||
"start" => engine.call_fn(scope, ast, "start", (event_data,)).unwrap(),
|
||||
|
||||
// The 'end' event maps to function 'end'.
|
||||
// In a real application you'd be handling errors...
|
||||
"end" => engine.call_fn(scope, ast, "end", (event_data,)).unwrap(),
|
||||
|
||||
// The 'update' event maps to function 'update'.
|
||||
// This event provides a default implementation when the scripted function is not found.
|
||||
"update" =>
|
||||
engine.call_fn(scope, ast, "update", (event_data,))
|
||||
.or_else(|err| match *err {
|
||||
EvalAltResult::ErrorFunctionNotFound(fn_name, _)
|
||||
if fn_name.starts_with("update") =>
|
||||
{
|
||||
// Default implementation of 'update' event handler.
|
||||
self.scope.set_value("obj_state", TestStruct::new(42));
|
||||
// Turn function-not-found into a success.
|
||||
Ok(Dynamic::UNIT)
|
||||
}
|
||||
_ => Err(err)
|
||||
}).unwrap(),
|
||||
|
||||
// In a real application you'd be handling unknown events...
|
||||
_ => panic!("unknown event: {}", event_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Scripting Styles
|
||||
----------------
|
||||
|
||||
Depending on needs and scripting style, there are three different ways to implement this pattern.
|
||||
|
||||
| | [Main style](events-1.md) | [JS style](events-2.md) | [Map style](events-3.md) |
|
||||
| ------------------------------------------------- | :---------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------: | :--------------------------------------------------------------------------: |
|
||||
| States store | custom [`Scope`] | [object map] bound to `this` | [object map] in custom [`Scope`] |
|
||||
| Access state [variable] | normal [variable] | [property][object map] of `this` | [property][object map] of state variable |
|
||||
| Access global [constants]? | yes | yes | yes |
|
||||
| Add new state [variable]? | `init` [function] only | all [functions] | all [functions] |
|
||||
| Add new global [constants]? | yes | **no** | **no** |
|
||||
| [OOP]-style [functions] on states? | **no** | yes | yes |
|
||||
| Detect system-provided initial states? | **no** | yes | yes |
|
||||
| Local [variable] may _[shadow]_ state [variable]? | yes | **no** | **no** |
|
||||
| Benefits | simple | fewer surprises | versatile |
|
||||
| Disadvantages | <ul><li>no new variables in [functions] (apart from `init`)</li><li>easy variable name collisions</li></ul> | <ul><li>`this.xxx` all over the place</li><li>more complex implementation</li></ul> | <ul><li>`state.xxx` all over the place</li><li>inconsistent syntax</li></ul> |
|
Reference in New Issue
Block a user