Skip to content

Commit

Permalink
feat: propagate event directive
Browse files Browse the repository at this point in the history
  • Loading branch information
RSamaium committed Dec 9, 2023
1 parent 472989a commit edbab50
Show file tree
Hide file tree
Showing 13 changed files with 228 additions and 40 deletions.
21 changes: 19 additions & 2 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,27 @@ const apiMenu = [
{ text: "GUI Class", link: "/classes/gui" },
{ text: "Sound Class", link: "/classes/sound" },
{ text: "Resource Class", link: "/classes/resource" },
{ text: "Keyboard Class", link: "/classes/keyboard" },
{ text: "Vue Inject Class", link: "/classes/vue-inject" }
{ text: "Keyboard Class", link: "/classes/keyboard" }
]

},
{
text: 'VueJS',
collapsed: false,
sidebarDepth: 2,
items: [
{ text: "Vue Inject Class", link: "/classes/vue-inject" },
{ text: "Vue directives", link: "/api-gui/vue-directive" }
]
},
{
text: 'React',
collapsed: false,
sidebarDepth: 2,
items: [
{ text: "React Hooks", link: "/api-gui/react" }
]
},
{
text: 'Testing',
collapsed: false,
Expand Down Expand Up @@ -252,6 +268,7 @@ module.exports = {
'/commands/': apiMenu,
'/database/': apiMenu,
'/api/': apiMenu,
'/api-gui/': apiMenu,
'/guide/': guideMenu,
'/gui/': guideMenu,
'/advanced/': guideMenu,
Expand Down
67 changes: 67 additions & 0 deletions docs/api-gui/react.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# React Hooks

## Introduction

React hooks are a powerful feature in React that allow you to use state and other React features without writing a class. In the context of RPGJS, a framework for creating RPG games, React hooks can be particularly useful for managing game state and interactions.

## 1. `useEventPropagator()`

This hook is used to propagate events within the canvas element of your RPGJS game.

### Importing

```javascript
import { useEventPropagator } from '@rpgjs/client/react';
```

### Usage

```javascript
export default function Test() {
const propagate = useEventPropagator();

return <div ref={propagate}>test</div>;
}
```

In this example, the `useEventPropagator` hook is used to create a `propagate` function. This function is then passed to a `div` element as a reference (`ref`). This setup allows events within the `div` to be propagated through the RPGJS game canvas.

---

## 2. `RpgReactContext`

This hook provides access to the RPGJS context, allowing you to interact with various game states like the current player's health points (HP).

### Importing

```javascript
import { RpgReactContext } from '@rpgjs/client/react';
import { useContext, useEffect, useState } from 'react';
```

### Usage

```javascript
export default function MyGUI({ foo }) {
const { rpgCurrentPlayer } = useContext(RpgReactContext);
const [hp, setHp] = useState(0);

useEffect(() => {
const subscription = rpgCurrentPlayer.subscribe(({ object }) => {
setHp(object.hp);
});

return () => {
subscription.unsubscribe();
};
}, []);

return (
<div>
<h1>{hp}</h1>
</div>
);
}
```

In this example, `RpgReactContext` is used to access the current player's state. The `useContext` hook retrieves the `rpgCurrentPlayer` from `RpgReactContext`. We then use `useState` to manage the player's HP locally. The `useEffect` hook is used to subscribe to changes in the player's HP, updating the local state accordingly. When the component unmounts, the subscription is unsubscribed.
17 changes: 17 additions & 0 deletions docs/api-gui/vue-directive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Directive for VueJS

## `v-propagate`

The `v-propagate` directive is straightforward to use. Simply add it to any element in your VueJS template to enable event propagation for that element within the RPGJS canvas.

### Example

```vue
<template>
<div v-propagate>
Test
</div>
</template>
```

In this example, the `v-propagate` directive is attached to a `div` element. Any events that occur within this `div` will be propagated through the RPGJS game canvas. This is particularly useful for integrating VueJS-based GUI elements with the RPGJS game canvas, allowing for seamless interaction between the GUI and the game.
16 changes: 15 additions & 1 deletion docs/guide/inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,18 @@ The key corresponds to the type of control (used by the keyboard, the mouse, or

You have information here: [Set Inputs](/classes/keyboard.html#set-inputs)

> If you want to use keyboard numbers, don't use "1" but "n1", etc.
::: tip Keyboard numbers
If you want to use keyboard numbers, don't use "1" but "n1", etc.
:::

::: tip Mouse Events
Since v4.2.0, you can use mouse events.

Example:

```toml
[inputs.action]
name = "action"
bind = ["space", "enter", "click"],
```
:::
27 changes: 26 additions & 1 deletion packages/client/src/Gui/React.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { createRoot } from 'react-dom/client';
import { createElement, Fragment, useState, createContext, useEffect, useContext, useCallback, useSyncExternalStore, useRef } from 'react'
import { RpgClientEngine } from '../RpgClientEngine';
import { RpgRenderer } from '../Renderer';
import { EVENTS_MAP, RpgRenderer } from '../Renderer';
import { BehaviorSubject, map, tap, combineLatest, Subject } from 'rxjs';
import type { Gui } from './Gui';
import { inject } from '../inject';

export { useStore } from '@nanostores/react'
export const RpgReactContext = createContext({} as any)
Expand Down Expand Up @@ -51,6 +52,30 @@ export const useCurrentPlayer = () => {
return useSyncExternalStore(subscribe, () => currentPlayerRef.current);
}

export const useEventPropagator = () => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
const element = ref.current as HTMLElement;
const eventListeners = {};
const renderer = inject(RpgRenderer)

EVENTS_MAP.MouseEvent.forEach(eventType => {
const listener = event => renderer.propagateEvent(event)
element.addEventListener(eventType, listener);
eventListeners[eventType] = listener;
});

return () => {
EVENTS_MAP.MouseEvent.forEach(eventType => {
element.removeEventListener(eventType, eventListeners[eventType]);
});
};
}
}, [ref]);
return ref
};

export class ReactGui {
private app: any
private clientEngine: RpgClientEngine
Expand Down
30 changes: 24 additions & 6 deletions packages/client/src/Gui/Vue.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, resolveDynamicComponent as _resolveDynamicComponent, normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, createBlock as _createBlock, mergeProps as _mergeProps, createCommentVNode as _createCommentVNode, normalizeStyle as _normalizeStyle, createElementVNode as _createElementVNode } from "vue"
import { App, ComponentPublicInstance, createApp } from 'vue'
import { RpgCommonPlayer, Utils } from '@rpgjs/common'
import { RpgRenderer } from '../Renderer'
import { EVENTS_MAP, RpgRenderer } from '../Renderer'
import { GameEngineClient } from '../GameEngine'
import { RpgClientEngine } from '../RpgClientEngine'
import type { Gui } from './Gui'
Expand Down Expand Up @@ -33,7 +33,7 @@ const _hoisted_1 = {
style: { "position": "absolute", "top": "0", "left": "0" }
}
function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", { }, [
return (_openBlock(), _createElementBlock("div", {}, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.fixedGui, (ui) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
(ui.display)
Expand All @@ -49,7 +49,7 @@ function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", {
style: _normalizeStyle(_ctx.tooltipPosition(tooltip.position))
}, [
(_openBlock(), _createBlock(_resolveDynamicComponent(ui.name), _mergeProps({ ...ui.data, spriteData: tooltip, style: { pointerEvents: 'auto' } }, {
(_openBlock(), _createBlock(_resolveDynamicComponent(ui.name), _mergeProps({ ...ui.data, spriteData: tooltip, style: { pointerEvents: 'auto' } }, {
ref_for: true,
ref: ui.name
}), null, 16 /* FULL_PROPS */))
Expand Down Expand Up @@ -99,9 +99,6 @@ export class VueGui {
methods: {
tooltipPosition: parentGui.tooltipPosition.bind(parentGui),
tooltipFilter: parentGui.tooltipFilter.bind(parentGui)
},
mounted() {

}
}

Expand All @@ -113,6 +110,27 @@ export class VueGui {
this.app.component(ui.name, ui.gui)
}

this.app.directive('propagate', {
mounted: (el, binding) => {
el.eventListeners = {};
EVENTS_MAP.MouseEvent.forEach(eventType => {
const callback = (ev) => {
this.renderer.propagateEvent(ev);
};
el.eventListeners[eventType] = callback;
el.addEventListener(eventType, callback);
});
},
unmounted(el, binding) {
EVENTS_MAP.MouseEvent.forEach(eventType => {
const callback = el.eventListeners[eventType];
if (callback) {
el.removeEventListener(eventType, callback);
}
});
}
})

this.vm = this.app.mount(rootEl) as VueInstance
this.renderer.app = this.app
this.renderer.vm = this.vm
Expand Down
24 changes: 24 additions & 0 deletions packages/client/src/KeyboardControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,18 @@ export class KeyboardControls {
return this.boundKeys[inputName]
}

/**
* Returns all controls
*
* @method getControls()
* @since 4.2.0
* @returns { { [key: string]: BoundKey } }
* @memberof KeyboardControls
*/
getControls(): { [key: string]: BoundKey } {
return this.boundKeys
}

/**
* Triggers an input according to the name of the control
*
Expand Down Expand Up @@ -509,6 +521,18 @@ export class KeyboardControls {
* Control.Action | action
* Control.Back | back
*
* @enum {string} Mouse Event
*
* click | Click
* dblclick | Double Click
* mousedown | Mouse Down
* mouseup | Mouse Up
* mouseover | Mouse Over
* mousemove | Mouse Move
* mouseout | Mouse Out
* contextmenu | Context Menu
*
*
* @enum {string} Input
*
* break | Pause
Expand Down
42 changes: 29 additions & 13 deletions packages/client/src/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Subject, forkJoin } from 'rxjs'
import { GameEngineClient } from './GameEngine'
import { SpinnerGraphic } from './Effects/Spinner'
import { autoDetectRenderer, Container, EventBoundary, FederatedEvent, FederatedPointerEvent, Graphics, ICanvas, IRenderer } from 'pixi.js'
import { KeyboardControls } from './KeyboardControls'

const { elementToPositionAbsolute } = Utils

Expand All @@ -22,6 +23,13 @@ enum ContainerName {
Map = 'map'
}

export const EVENTS_MAP = {
MouseEvent: ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'contextmenu', 'wheel'],
KeyboardEvent: ['keydown', 'keyup', 'keypress', 'keydownoutside', 'keyupoutside', 'keypressoutside'],
PointerEvent: ['pointerdown', 'pointerup', 'pointermove', 'pointerover', 'pointerout', 'pointerenter', 'pointerleave', 'pointercancel'],
TouchEvent: ['touchstart', 'touchend', 'touchmove', 'touchcancel']
};

export class RpgRenderer {
private gameEngine: GameEngineClient = this.context.inject(GameEngineClient)
private clientEngine: RpgClientEngine = this.context.inject(RpgClientEngine)
Expand Down Expand Up @@ -83,8 +91,8 @@ export class RpgRenderer {
this.spinner.y = h * 0.5
}

get canvas(): ICanvas {
return this.renderer.view
get canvas(): HTMLCanvasElement {
return this.renderer.view as HTMLCanvasElement
}

get height(): number {
Expand Down Expand Up @@ -140,6 +148,21 @@ export class RpgRenderer {
await RpgGui._initialize(this.context, this.guiEl)

this.resize()
this.bindMouseControls()

}

private bindMouseControls() {
const controlInstance = this.context.inject(KeyboardControls)
const controls = controlInstance.getControls()
for (let key in controls) {
const { actionName } = controls[key]
if (EVENTS_MAP.MouseEvent.includes(key)) {
this.canvas.addEventListener(key, (e) => {
controlInstance.applyControl(actionName)
})
}
}
}

/** @internal */
Expand Down Expand Up @@ -275,8 +298,7 @@ export class RpgRenderer {
* @returns {void}
*/
propagateEvent(ev: MouseEvent) {
const canvas = this.renderer.view as HTMLCanvasElement;
const rect = canvas.getBoundingClientRect();
const rect = this.canvas.getBoundingClientRect();
const canvasX = rect.left + window.scrollX;
const canvasY = rect.top + window.scrollY;
const realX = ev.clientX - canvasX;
Expand All @@ -286,7 +308,8 @@ export class RpgRenderer {
event.global.set(realX, realY);
event.type = ev.type;
const hitTestTarget = boundary.hitTest(realX, realY);
hitTestTarget?.dispatchEvent(event);
hitTestTarget?.dispatchEvent(event)
this.canvas.dispatchEvent(new MouseEvent(ev.type, ev))
}

/***
Expand All @@ -299,14 +322,7 @@ export class RpgRenderer {
* @returns {void}
*/
addPropagateEventsFrom(el: HTMLElement) {
const eventMap = {
MouseEvent: ['click', 'mousedown', 'mouseup', 'mousemove', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'contextmenu', 'wheel'],
KeyboardEvent: ['keydown', 'keyup', 'keypress', 'keydownoutside', 'keyupoutside', 'keypressoutside'],
PointerEvent: ['pointerdown', 'pointerup', 'pointermove', 'pointerover', 'pointerout', 'pointerenter', 'pointerleave', 'pointercancel'],
TouchEvent: ['touchstart', 'touchend', 'touchmove', 'touchcancel']
};

for (let [_Constructor, events] of Object.entries(eventMap)) {
for (let [_Constructor, events] of Object.entries(EVENTS_MAP)) {
for (let type of events) {
el.addEventListener(type, (e) => {
const _class = window[_Constructor] ?? MouseEvent
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/RpgClientEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export class RpgClientEngine {
this.addSound(sound, id)
})

// obsolete
// deprecated
if (typeof __RPGJS_PRODUCTION__ != 'undefined' && __RPGJS_PRODUCTION__) {
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
Expand Down
Loading

0 comments on commit edbab50

Please sign in to comment.