@arancini/react
Advanced tools
Comparing version
@@ -30,3 +30,3 @@ import * as A from '@arancini/core'; | ||
export declare type QueryEntitiesProps = { | ||
query: A.QueryDescription; | ||
query: A.Query | A.QueryDescription; | ||
children: ReactNode | ((entity: A.Entity) => ReactNode); | ||
@@ -43,9 +43,9 @@ }; | ||
Entities: ({ entities, children }: EntitiesProps) => JSX.Element; | ||
QueryEntities: ({ query: queryDescription, children, }: QueryEntitiesProps) => JSX.Element; | ||
QueryEntities: ({ query, children }: QueryEntitiesProps) => JSX.Element; | ||
Component: <T extends A.Component>({ args, children, type, }: ComponentProps<T>) => React.ReactElement<any, string | React.JSXElementConstructor<any>> | null; | ||
System: <T_1 extends A.System>({ type, priority }: SystemProps<T_1>) => null; | ||
useQuery: (queryDescription: A.QueryDescription) => A.Query; | ||
useQuery: (q: A.Query | A.QueryDescription) => A.Query; | ||
useCurrentEntity: () => EntityProviderContext; | ||
useCurrentSpace: () => A.Space; | ||
step: (delta: number) => void; | ||
update: (delta: number) => void; | ||
world: A.World; | ||
@@ -52,0 +52,0 @@ spaceContext: React.Context<SpaceProviderContext>; |
@@ -54,6 +54,6 @@ import * as A from "@arancini/core"; | ||
const world = existing != null ? existing : new A.World(); | ||
if (!existing) { | ||
if (!world.initialised) { | ||
world.init(); | ||
} | ||
const step = (delta) => { | ||
const update = (delta) => { | ||
world.update(delta); | ||
@@ -70,3 +70,3 @@ }; | ||
}) => { | ||
const [space, setSpace] = useState(null); | ||
const [context, setContext] = useState(null); | ||
useIsomorphicLayoutEffect(() => { | ||
@@ -76,5 +76,7 @@ const newSpace = world.create.space({ | ||
}); | ||
setSpace(newSpace); | ||
setContext({ | ||
space: newSpace | ||
}); | ||
return () => { | ||
setSpace(null); | ||
setContext(null); | ||
newSpace.destroy(); | ||
@@ -84,5 +86,3 @@ }; | ||
return /* @__PURE__ */ jsx(spaceContext.Provider, { | ||
value: { | ||
space | ||
}, | ||
value: context, | ||
children | ||
@@ -96,16 +96,23 @@ }); | ||
const space = useCurrentSpace(); | ||
const [entity, setEntity] = useState(null); | ||
const [context, setContext] = useState(null); | ||
useIsomorphicLayoutEffect(() => { | ||
if (existingEntity) { | ||
setEntity(existingEntity); | ||
if (!space) { | ||
return; | ||
} | ||
if (!space) { | ||
if (existingEntity) { | ||
setContext({ | ||
entity: existingEntity | ||
}); | ||
return; | ||
} | ||
const newEntity = space.create.entity(); | ||
setEntity(newEntity); | ||
setContext({ | ||
entity: newEntity | ||
}); | ||
const { | ||
id | ||
} = newEntity; | ||
return () => { | ||
setEntity(null); | ||
if (newEntity.alive) { | ||
setContext(null); | ||
if (id === newEntity.id) { | ||
newEntity.destroy(); | ||
@@ -116,5 +123,3 @@ } | ||
return /* @__PURE__ */ jsx(entityContext.Provider, { | ||
value: { | ||
entity | ||
}, | ||
value: context, | ||
children | ||
@@ -133,6 +138,9 @@ }); | ||
}); | ||
const useQuery = (queryDescription) => { | ||
const useQuery = (q2) => { | ||
const query = useMemo(() => { | ||
return world.create.query(queryDescription); | ||
}, [queryDescription]); | ||
if (q2 instanceof A.Query) { | ||
return q2; | ||
} | ||
return world.create.query(q2); | ||
}, [q2]); | ||
const rerender = useRerender(); | ||
@@ -151,8 +159,10 @@ useIsomorphicLayoutEffect(() => { | ||
const QueryEntities = ({ | ||
query: queryDescription, | ||
query, | ||
children | ||
}) => { | ||
const query = useQuery(queryDescription); | ||
const { | ||
entities | ||
} = useQuery(query); | ||
return /* @__PURE__ */ jsx(Entities, { | ||
entities: query.entities, | ||
entities, | ||
children | ||
@@ -167,9 +177,10 @@ }); | ||
const ref = useRef(null); | ||
const { | ||
entity | ||
} = useContext(entityContext); | ||
const entityCtx = useContext(entityContext); | ||
useIsomorphicLayoutEffect(() => { | ||
if (!entity || !entity.alive) { | ||
if (!entityCtx || !entityCtx.entity.space) { | ||
return; | ||
} | ||
const { | ||
entity | ||
} = entityCtx; | ||
let newComponent; | ||
@@ -182,7 +193,7 @@ if (children) { | ||
return () => { | ||
if (entity.has(newComponent.__internal.class)) { | ||
if (entity.find(newComponent._class) === newComponent) { | ||
entity.remove(newComponent); | ||
} | ||
}; | ||
}, [entity, args, children, type]); | ||
}, [entityCtx, args, children, type]); | ||
if (children) { | ||
@@ -220,3 +231,3 @@ const child = React.Children.only(children); | ||
useCurrentSpace, | ||
step, | ||
update, | ||
world, | ||
@@ -223,0 +234,0 @@ spaceContext, |
@@ -1,2 +0,2 @@ | ||
(function(l,y){typeof exports=="object"&&typeof module!="undefined"?y(exports,require("@arancini/core"),require("react")):typeof define=="function"&&define.amd?define(["exports","@arancini/core","react"],y):(l=typeof globalThis!="undefined"?globalThis:l||self,y(l.index={},l.A,l.React))})(this,function(l,y,c){"use strict";function w(e){return e&&typeof e=="object"&&"default"in e?e:{default:e}}function h(e){if(e&&e.__esModule)return e;var r=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});return e&&Object.keys(e).forEach(function(s){if(s!=="default"){var u=Object.getOwnPropertyDescriptor(e,s);Object.defineProperty(r,s,u.get?u:{enumerable:!0,get:function(){return e[s]}})}}),r.default=e,Object.freeze(r)}var P=h(y),S=w(c);const d=typeof window!="undefined"?c.useLayoutEffect:c.useEffect,T=e=>r=>{e.forEach(s=>{typeof s=="function"?s(r):s&&(s.current=r)})};function q(){const[e,r]=c.useState(0);return c.useCallback(()=>{r(s=>s+1)},[])}var C={exports:{}},E={};/** | ||
(function(l,m){typeof exports=="object"&&typeof module!="undefined"?m(exports,require("@arancini/core"),require("react")):typeof define=="function"&&define.amd?define(["exports","@arancini/core","react"],m):(l=typeof globalThis!="undefined"?globalThis:l||self,m(l.index={},l.A,l.React))})(this,function(l,m,c){"use strict";function h(t){return t&&typeof t=="object"&&"default"in t?t:{default:t}}function T(t){if(t&&t.__esModule)return t;var r=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});return t&&Object.keys(t).forEach(function(s){if(s!=="default"){var o=Object.getOwnPropertyDescriptor(t,s);Object.defineProperty(r,s,o.get?o:{enumerable:!0,get:function(){return t[s]}})}}),r.default=t,Object.freeze(r)}var O=T(m),x=h(c);const p=typeof window!="undefined"?c.useLayoutEffect:c.useEffect,L=t=>r=>{t.forEach(s=>{typeof s=="function"?s(r):s&&(s.current=r)})};function A(){const[t,r]=c.useState(0);return c.useCallback(()=>{r(s=>s+1)},[])}var C={exports:{}},S={};/** | ||
* @license React | ||
@@ -9,2 +9,2 @@ * react-jsx-runtime.production.min.js | ||
* LICENSE file in the root directory of this source tree. | ||
*/var L=S.default,A=Symbol.for("react.element"),I=Symbol.for("react.fragment"),M=Object.prototype.hasOwnProperty,k=L.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,F={key:!0,ref:!0,__self:!0,__source:!0};function x(e,r,s){var u,p={},_=null,v=null;s!==void 0&&(_=""+s),r.key!==void 0&&(_=""+r.key),r.ref!==void 0&&(v=r.ref);for(u in r)M.call(r,u)&&!F.hasOwnProperty(u)&&(p[u]=r[u]);if(e&&e.defaultProps)for(u in r=e.defaultProps,r)p[u]===void 0&&(p[u]=r[u]);return{$$typeof:A,type:e,key:_,ref:v,props:p,_owner:k.current}}E.Fragment=I,E.jsx=x,E.jsxs=x,C.exports=E;const m=C.exports.jsx,N=C.exports.Fragment,D=e=>{const r=c.createContext(null),s=c.createContext(null),u=e!=null?e:new P.World;e||u.init();const p=t=>{u.update(t)},_=()=>c.useContext(s),v=()=>{const t=c.useContext(r);return t?t.space:u.defaultSpace},Q=({id:t,children:n})=>{const[o,a]=c.useState(null);return d(()=>{const i=u.create.space({id:t});return a(i),()=>{a(null),i.destroy()}},[t]),m(r.Provider,{value:{space:o},children:n})},U=({children:t,entity:n})=>{const o=v(),[a,i]=c.useState(null);return d(()=>{if(n){i(n);return}if(!o)return;const f=o.create.entity();return i(f),()=>{i(null),f.alive&&f.destroy()}},[o]),m(s.Provider,{value:{entity:a},children:t})},O=c.memo(U),b=({entities:t,children:n})=>m(N,{children:t.map(o=>m(O,{entity:o,children:typeof n=="function"?n(o):n},o.id))}),j=t=>{const n=c.useMemo(()=>u.create.query(t),[t]),o=q();return d(()=>(n.onEntityAdded.add(o),n.onEntityRemoved.add(o),()=>{n.onEntityAdded.remove(o),n.onEntityRemoved.remove(o)}),[o]),d(o,[]),n};return{Space:Q,Entity:O,Entities:b,QueryEntities:({query:t,children:n})=>{const o=j(t);return m(b,{entities:o.entities,children:n})},Component:({args:t,children:n,type:o})=>{const a=c.useRef(null),{entity:i}=c.useContext(s);if(d(()=>{if(!i||!i.alive)return;let f;return n?f=i.add(o,a.current):f=i.add(o,...t!=null?t:[]),()=>{i.has(f.__internal.class)&&i.remove(f)}},[i,t,n,o]),n){const f=S.default.Children.only(n);return S.default.cloneElement(f,{ref:T([f.ref,a])})}return null},System:({type:t,priority:n})=>(d(()=>(u.registerSystem(t,{priority:n}),()=>{u.unregisterSystem(t)}),[t,n]),null),useQuery:j,useCurrentEntity:_,useCurrentSpace:v,step:p,world:u,spaceContext:r,entityContext:s}};l.createECS=D,Object.defineProperties(l,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}); | ||
*/var I=x.default,M=Symbol.for("react.element"),k=Symbol.for("react.fragment"),D=Object.prototype.hasOwnProperty,F=I.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,N={key:!0,ref:!0,__self:!0,__source:!0};function b(t,r,s){var o,y={},E=null,v=null;s!==void 0&&(E=""+s),r.key!==void 0&&(E=""+r.key),r.ref!==void 0&&(v=r.ref);for(o in r)D.call(r,o)&&!N.hasOwnProperty(o)&&(y[o]=r[o]);if(t&&t.defaultProps)for(o in r=t.defaultProps,r)y[o]===void 0&&(y[o]=r[o]);return{$$typeof:M,type:t,key:E,ref:v,props:y,_owner:F.current}}S.Fragment=k,S.jsx=b,S.jsxs=b,C.exports=S;const _=C.exports.jsx,Q=C.exports.Fragment,g=t=>{const r=c.createContext(null),s=c.createContext(null),o=t!=null?t:new O.World;o.initialised||o.init();const y=e=>{o.update(e)},E=()=>c.useContext(s),v=()=>{const e=c.useContext(r);return e?e.space:o.defaultSpace},q=({id:e,children:n})=>{const[u,d]=c.useState(null);return p(()=>{const f=o.create.space({id:e});return d({space:f}),()=>{d(null),f.destroy()}},[e]),_(r.Provider,{value:u,children:n})},U=({children:e,entity:n})=>{const u=v(),[d,f]=c.useState(null);return p(()=>{if(!u)return;if(n){f({entity:n});return}const i=u.create.entity();f({entity:i});const{id:a}=i;return()=>{f(null),a===i.id&&i.destroy()}},[u]),_(s.Provider,{value:d,children:e})},j=c.memo(U),w=({entities:e,children:n})=>_(Q,{children:e.map(u=>_(j,{entity:u,children:typeof n=="function"?n(u):n},u.id))}),P=e=>{const n=c.useMemo(()=>e instanceof O.Query?e:o.create.query(e),[e]),u=A();return p(()=>(n.onEntityAdded.add(u),n.onEntityRemoved.add(u),()=>{n.onEntityAdded.remove(u),n.onEntityRemoved.remove(u)}),[u]),p(u,[]),n};return{Space:q,Entity:j,Entities:w,QueryEntities:({query:e,children:n})=>{const{entities:u}=P(e);return _(w,{entities:u,children:n})},Component:({args:e,children:n,type:u})=>{const d=c.useRef(null),f=c.useContext(s);if(p(()=>{if(!f||!f.entity.space)return;const{entity:i}=f;let a;return n?a=i.add(u,d.current):a=i.add(u,...e!=null?e:[]),()=>{i.find(a._class)===a&&i.remove(a)}},[f,e,n,u]),n){const i=x.default.Children.only(n);return x.default.cloneElement(i,{ref:L([i.ref,d])})}return null},System:({type:e,priority:n})=>(p(()=>(o.registerSystem(e,{priority:n}),()=>{o.unregisterSystem(e)}),[e,n]),null),useQuery:P,useCurrentEntity:E,useCurrentSpace:v,update:y,world:o,spaceContext:r,entityContext:s}};l.createECS=g,Object.defineProperties(l,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}); |
{ | ||
"name": "@arancini/react", | ||
"description": "React glue for the 'arancini' object based Entity Component System", | ||
"keywords": [ | ||
"react", | ||
"gamedev", | ||
"ecs", | ||
"entity-component-system" | ||
], | ||
"type": "module", | ||
@@ -7,3 +14,3 @@ "packageManager": "yarn@3.2.1", | ||
"license": "MIT", | ||
"version": "0.1.1", | ||
"version": "1.0.0", | ||
"scripts": { | ||
@@ -15,4 +22,14 @@ "dev": "vite", | ||
"build-storybook": "build-storybook", | ||
"test": "jest" | ||
"test": "jest --coverage" | ||
}, | ||
"dependencies": { | ||
"@arancini/core": "^1.0.0", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0" | ||
}, | ||
"peerDependencies": { | ||
"@arancini/core": "^1.0.0", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0" | ||
}, | ||
"devDependencies": { | ||
@@ -59,13 +76,3 @@ "@babel/core": "^7.18.6", | ||
} | ||
}, | ||
"dependencies": { | ||
"@arancini/core": "0.1.1", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0" | ||
}, | ||
"peerDependencies": { | ||
"arancini": "0.1.1", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0" | ||
} | ||
} |
116
README.md
@@ -13,3 +13,3 @@ # @arancini/react | ||
To get started, use `createECS` to get React glue scoped to a arancini world. Because components and hooks are scoped, libraries can use them without worrying about context conflicts. | ||
To get started, use `createECS` to get glue components and hooks scoped to a given arancini world. Because the react glue is scoped, libraries can use @arancini/react without worrying about context conflicts. | ||
@@ -25,4 +25,4 @@ ```ts | ||
```ts | ||
import { createECS } from '@arancini/react' | ||
import { World } from 'arancini' | ||
import { createECS } from '@arancini/react' | ||
@@ -35,3 +35,3 @@ const world = new World() | ||
The `createECS` function returns a reference to the arancini world. If you didn't pass in an existing world, you can still use the regular imperative API directly. | ||
`createECS` returns a reference to the arancini world, so if you didn't pass `createECS` in an existing world, you can still use the regular imperative API. | ||
@@ -42,3 +42,8 @@ ```ts | ||
// use the World as normal | ||
/* use the world as normal */ | ||
// register components | ||
world.registerComponent(MyComponent) | ||
// create entities | ||
const entity = world.create.entity() | ||
@@ -49,3 +54,3 @@ ``` | ||
`@arancini/react` does not automatically step the World for you, the `step` method must be called. If you are using arancini with `@react-three/fiber`, you can use the `useFrame` hook to step the World. | ||
`@arancini/react` does not automatically step the world for you. If you are using arancini with `@react-three/fiber`, you can use the `useFrame` hook to step the World. | ||
@@ -57,3 +62,3 @@ ```tsx | ||
useFrame((_, delta) => { | ||
ECS.step(delta) | ||
ECS.update(delta) | ||
}) | ||
@@ -65,6 +70,25 @@ | ||
### Creating Entities and Components | ||
If arancini needs to be integrated into an existing game loop, instead of calling `step`, you can decide when to update parts of the world. | ||
The `Entity` can be used to declaratively create entities, and `Component` can be used to add components to an entity. | ||
```tsx | ||
const ECS = createECS() | ||
const Example = () => { | ||
useFrame(({ clock: { elapsedTime }, delta) => { | ||
// update all systems | ||
this.systemManager.update(delta, elapsedTime) | ||
// or update a particular system | ||
const exampleSystem = ECS.world.getSystem(ExampleSystem) | ||
exampleSystem.update(delta, elapsedTime) | ||
}) | ||
} | ||
``` | ||
### Spaces, Entities, Components | ||
`<Space />` can be used to declaratively create spaces, `<Entity />` can be used to declaratively create entities, and `<Component />` can be used to add components to an entity. | ||
`<Component />` will automatically register the component with the world if it hasn't been registered yet. | ||
```tsx | ||
@@ -82,9 +106,11 @@ class Position extends A.Component { | ||
const Example = () => ( | ||
<ECS.Entity> | ||
<ECS.Component type={Position} args={[0, 0]} /> | ||
</ECS.Entity> | ||
<ECS.Space> | ||
<ECS.Entity> | ||
<ECS.Component type={Position} args={[0, 0]} /> | ||
</ECS.Entity> | ||
</ECS.Space> | ||
) | ||
``` | ||
You can also pass an existing entity to `Entity`. | ||
You can also pass an existing entity to `<Entity />`. | ||
@@ -102,2 +128,26 @@ ```tsx | ||
`@arancini/react` also provides an `<Entities />` component that can be used to render a list of entities or add components to existing entities. `<Entities />` also supports [render props](https://reactjs.org/docs/render-props.html). | ||
```tsx | ||
const SimpleExample = () => ( | ||
<ECS.Entities entities={[entity1, entity2]}> | ||
{/* ... */} | ||
</ECS.Entities> | ||
) | ||
const AddComponentToEntities = () => ( | ||
<ECS.Entities entities={[entity1, entity2]}> | ||
<ECS.Component type={Position} args={[0, 0]} /> | ||
</ECS.Entities> | ||
) | ||
const RenderProps = () => ( | ||
<ECS.Entities entities={[entity1, entity2]}> | ||
{(entity) => { | ||
// ... | ||
}} | ||
</ECS.Entities> | ||
) | ||
``` | ||
### Querying the world | ||
@@ -107,3 +157,3 @@ | ||
The `useQuery` hook queries the world for entities with given components, and will re-render when the query results change. | ||
The `useQuery` hook queries the world for entities with given components and will re-render when the query results change. | ||
@@ -173,4 +223,2 @@ ```tsx | ||
) | ||
const | ||
``` | ||
@@ -205,6 +253,38 @@ | ||
## Advanced | ||
### Systems | ||
### Using Entity and Space contexts | ||
`@arancini/react` provides a `<System />` helper component that can be used to register a system. It will automatically unregister the system when the component unmounts. | ||
```tsx | ||
import * as A from 'arancini' | ||
class ExampleSystem extends A.System { | ||
update(delta: number) { | ||
// ... | ||
} | ||
} | ||
const Example = () => ( | ||
<ECS.System type={ExampleSystem} priority={10} /> | ||
) | ||
``` | ||
You can also opt to register systems using the imperative API. | ||
```tsx | ||
const Example = () => { | ||
useEffect(() => { | ||
ECS.world.registerSystem(ExampleSystem, { priority: 10 }) | ||
return () => { | ||
ECS.world.unregisterSystem(ExampleSystem) | ||
} | ||
}, []) | ||
} | ||
``` | ||
## Advanced Usage | ||
### Entity and Space contexts | ||
You can use the hooks `useCurrentEntitiy` and `useCurrentSpace` to access the current entity and space in a React component. | ||
@@ -244,2 +324,2 @@ | ||
For truly advanced usage, `createECS` also returns the contexts themselves, `entityContext` and `spaceContext`. | ||
For truly advanced usage, `createECS` also returns the react contexts, `entityContext` and `spaceContext`. |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
22189
12.61%305
3.74%2
-33.33%313
34.33%+ Added
- Removed
- Removed
Updated