Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

🧪 Mana Potion – Toolkit for JavaScript game development and interactive experiences (React, Vue, Svelte, vanilla)

License

NotificationsYou must be signed in to change notification settings

verekia/manapotion

Repository files navigation

Mana Potion

🧪Mana Potion is a toolkit for JavaScript game development and interactive experiences. It isnot a game engine or framework but a collection oflow-level utilities and helpers commonly needed when building games.

Mana Potion supports React, Vue, Svelte, and vanilla JavaScript. It is a particularly great fit for people who build games or experiences inReact Three Fiber,TresJS,Threlte, and vanillaThree.js, but it can be used in any context.

The library consists of:

Important: Until we hit 1.0.0, expect breaking changes in minor versions.

Demos

Check out theReact,Vue,Svelte, andvanilla JS demos.

Installation

  • If you useReact, install@manapotion/react
  • If you useVue, install@manapotion/vue
  • If you useSvelte, install@manapotion/svelte
  • If you don't use these frameworks, install@manapotion/vanilla

Getting started

Add<Listeners /> somewhere in your app:

React, Vue, Svelte

import{Listeners}from'@manapotion/react'// or vue, svelteconstApp=()=>(<><div>Your game</div><Listeners/></>)

Vanilla

import{listeners}from'@manapotion/vanilla'constunsub=listeners({})// call unsub() to stop listening

This will automatically give you access to some reactive and non-reactive variables. If you do not want to listen to every event supported by the library, you can cherry-pick individual listeners (for example,<MouseMoveListener /> or<FullscreenListener />).

🗿Non-reactive variables may be frequently updated and should be accessed imperatively in your main loop or in event handlers viagetMouse,getKeyboard, andgetBrowser:

import{getMouse,getKeyboard,getBrowser}from'@manapotion/react'// or vue, svelte, vanillaconstanimate=()=>{const{ right}=getMouse().buttonsconst{ KeyW}=getKeyboard().codesconst{ isFullscreen}=getBrowser()// ...}

⚡️Reactive variables can be accessed imperatively too, but also reactively in components to trigger re-renders:

React

Use theuseMouse,useKeyboard, anduseBrowser hooks with a selector to access variables reactively:

import{useMouse,useBrowser,useKeyboard}from'@manapotion/react'constComponent=()=>{constisRightButtonDown=useMouse(s=>s.buttons.right)const{ KeyW}=useKeyboard(s=>s.codes)constisFullscreen=useBrowser(s=>s.isFullscreen)// Some reactive componentreturn(/* ... */)}

Vue

<script setup lang="ts">import {mouse,browser,keyboard }from'@manapotion/vue'</script><template>  <div>{{ mouse.buttons.right }}</div>  <div>{{ browser.isFullscreen }}</div>  <div>{{ keyboard.codes.KeyW }}</div></template>

Svelte

<scriptlang="ts">import {mouse,browser,keyboard }from'@manapotion/svelte'</script>  <div>{$mouse.buttons.right}</div>  <div>{$browser.isFullscreen}</div>  <div>{$keyboard.codes.KeyW}</div>

Vanilla

There is no reactivity system in vanilla JavaScript, so you can usecallbacks to update your app state when the store changes. You can also subscribe to the Zustand store directly to watch for changes:

import{mouseStore}from'@manapotion/vanilla'constunsub=mouseStore.subscribe(state=>{console.log(state.buttons.right)})

Here are the variables available:

Legend: ⚡️Reactive, 🗿Non-reactive, 🚧Not implemented yet

🌐 Browser

  • ⚡️browser.isFullscreen
  • ⚡️browser.isPageVisible
  • ⚡️browser.isPageFocused
  • ⚡️browser.isDesktop /browser.isMobile
  • ⚡️browser.isLandscape /browser.isPortrait
  • 🗿browser.width
  • 🗿browser.height
  • 🚧pointerLockSupported

🖱️ Mouse

  • ⚡️mouse.buttons.left
  • ⚡️mouse.buttons.middle
  • ⚡️mouse.buttons.right
  • ⚡️mouse.locked
  • 🗿mouse.position.x
  • 🗿mouse.position.y (the bottom of the screen is 0)
  • 🗿mouse.movement.x (reset aftermouseMovementResetDelay)
  • 🗿mouse.movement.y (going up is positive)
  • 🗿mouse.wheel.y (delta, reset aftermouseScrollResetDelay)

You can import and useresetMouse to reinitialize the mouse data.

⌨️ Keyboard

  • ⚡️keyboard.codes
  • ⚡️keyboard.keys
  • ⚡️keyboard.ctrl
  • ⚡️keyboard.shift
  • ⚡️keyboard.alt
  • ⚡️keyboard.meta

⚡️keyboard contains keys that are available in two versions,codes andkeys. This lets you decide if you want to use thephysical location (codes) of the key or the character being typed as a key (keys). Using the physical location is better for game controls such as using WASD to move a character, because it is agnostic to the user's keyboard layout (did you know French keyboards are not QWERTY but AZERTY?).

Here is how you would handle going forward when the user presses W (or Z on French keyboards):

constanimate=()=>{const{ KeyW}=getKeyboard().codesif(KeyW){// Go forward}}

For keyboard events, just like all other events, you can add a custom callback to<Listeners />:

constApp=()=>{consthandleKeyDown=e=>{if(e.code==='Space'){jump()}}return(<><div>Your game</div><ListenersonKeyDown={handleKeyDown}/></>)}

You can import and useresetKeyboard to reinitialize the keyboard data.

This is useful to prevent keys from staying pressed when switching between tabs or when the game loses focus:

import{Listeners,resetKeyboard,resetMouse}from'@manapotion/react'constApp=()=>(<ListenersonPageFocusChange={()=>{resetKeyboard()resetMouse()}}onPageVisibilityChange={()=>{resetKeyboard()resetMouse()}}/>)

If your game requires holding a key to perform some action, this technique can prevent players cheating by holding the key and switching tabs.

Callbacks

You can provide custom event callbacks to<Listeners /> or to individual listeners:

React

<ListenersonFullscreenChange={handleFullscreenChange}/>/* or */<FullscreenListeneronFullscreenChange={handleFullscreenChange}/>

Vue

<Listeners @fullscreenChange="handleFullscreenChange" /><!-- or --><FullscreenListener @fullscreen-change="handleFullscreenChange" />

Svelte

<Listenerson:fullscreenChange={handleFullscreenChange} /><!-- or --><FullscreenListeneron:fullscreenChange={handleFullscreenChange} />

Vanilla

listeners({onFullscreenChange:handleFullscreenChange})// ormountFullscreenListener({onFullscreenChange:handleFullscreenChange})

Please check the TypeScript types for the available callbacks.

Once mounted, you cannot modify the callbacks dynamically. If you need to change them, you will need to unmount and remount the component. If you have use cases of callbacks changed dynamically, please let me know onDiscord.

Main loop

TheuseMainLoop hook can be used to schedule your various systems in a singlerequestAnimationFrame call that you can configure per component:

React

import{useRef}from'react'import{useMainLoop}from'@manapotion/react'importplayerfrom'./player'constPlayer=()=>{constref=useRef<HTMLDivElement>(null)useMainLoop(({ delta, elapsed})=>{ref.current!.style.transform=`translate(${player.x}px,${player.y}px)`})return<divref={ref}>Player</div>}

Vue

<script setup lang="ts">import {ref }from'vue'import {useMainLoop }from'@manapotion/vue'importplayerfrom'./player'const playerRef=ref<HTMLDivElement|null>(null)useMainLoop(({delta,elapsed })=> {playerRef.value!.style.transform=`translate(${player.x}px, ${player.y}px)`})</script><template>  <divref="playerRef">Player</div></template>

Svelte

<scriptlang="ts">import {useMainLoop }from'@manapotion/svelte'importplayerfrom'./player'let playerEl:HTMLDivElementuseMainLoop(({delta,elapsed })=> {playerEl.style.transform=`translate(${player.x}px, ${player.y}px)`  })</script><divbind:this={playerEl}>Player</div>

Vanilla

import{addMainLoopEffect}from'@manapotion/vanilla'constunsub=addMainLoopEffect(({ delta, elapsed})=>{// Your animation loop})// call unsub() to stop the animation loop

Throttling

You can throttle some callbacks by passing athrottle option touseMainLoop/addMainLoopEffect:

useMainLoop(({ delta, elapsed})=>{// Your animation loop},{throttle:100}// ms)

Stages

Organize your main loop into stages to run your systems in a specific order (using arbitrary numbers):

exportconstSTAGE_CONTROLS=-5exportconstSTAGE_PHYSICS=-4exportconstSTAGE_LOGIC=-2exportconstSTAGE_RENDER=0// Default stageexportconstSTAGE_UI=5exportconstSTAGE_CLEANUP=10constHealthBar=()=>{useMainLoop(()=>{// Adjust health bar width},{stage:STAGE_UI,throttle:100})}constPhysics=()=>{useMainLoop(()=>{// Update physics},{stage:STAGE_PHYSICS})}

You can pause and resume the main loop withpauseMainLoop andresumeMainLoop:

<ListenersonPageVisibilityChange={({ isPageVisible})=>{isPageVisible ?resumeMainLoop() :pauseMainLoop()}}/>

If you are using React Three Fiber, you can disable R3F's loop and sync the canvas with Mana Potion's loop by settingframeloop="never" on your<Canvas> and adding the following component as its child:

constSyncMainLoop=()=>{constadvance=useThree(s=>s.advance)useMainLoop(({ time})=>advance(time/1000),{stage:STAGE_RENDER}// Or whatever stage you want)returnnull}// ...constApp=()=>(<Canvasframeloop="never"><SyncMainLoop/>{/* Your scene */}</Canvas>)

Similarly, if you are using Threlte, setrenderMode tomanual on yourCanvas and calluseThrelte().advance() in auseMainLoop.

TresJS will supportconditional rendering inv4.

Virtual joysticks

Mana Potion includes🗿 non-reactive andheadless virtual joysticks for mobile controls. Each virtual joystick is associated with a single<JoystickArea />. You can create your own Joystick objects withcreateJoystick() or use one of the two default ones that are already available on the joysticks store. The default ones are calledmovement androtation joysticks.

You can choose between 2 modes,follow ororigin, and can adjust themaxFollowDistance ormaxOriginDistance. Use theonStart,onMove, andonEnd callbacks to update your game state and optionally show a joystick on the screen.

import{JoystickArea,getJoysticks}from'@manapotion/react'constMobileUI=()=>(<JoystickAreajoystick={getJoysticks().movement}mode="follow"// DefaultmaxFollowDistance={50}// DefaultonStart={handleStart}onMove={handleMove}onEnd={handleEnd}/>)

With vanilla JS, usemountJoystickArea instead.

In follow mode, the joystick will follow the user's finger, which is good for player movement.

Here are the properties that will be updated on your joystick object:

  • 🗿joystick.isActive
  • 🗿joystick.identifier
  • 🗿joystick.origin.x /joystick.origin.y
  • 🗿joystick.origin.angle
  • 🗿joystick.origin.distance
  • 🗿joystick.origin.distanceRatio
  • 🗿joystick.follow.x /joystick.follow.y
  • 🗿joystick.follow.angle
  • 🗿joystick.follow.distance
  • 🗿joystick.follow.distanceRatio
  • 🗿joystick.current.x/joystick.current.y
  • 🗿joystick.movement.x /joystick.movement.y

See theexample of how to style your joystick.

Multitouch within a single area is not supported, but you can create multiple<JoystickArea />. One for movement and one for camera rotation for example.

Browser API Helpers

Mana Potion provides helper functions to reduce some browser APIs boilerplate:

  • enterFullscreen
  • exitFullscreen
  • lockOrientation
  • unlockOrientation
  • lockPointer
  • unlockPointer
  • lockKeys
  • unlockKeys

For a fully immersive experience of an FPS game for example, when the player clicks Play or the Fullscreen button, you might want to call multiple helpers in a row like this:

import{enterFullscreen,exitFullscreen,lockOrientation,unlockOrientation,lockKeys,unlockKeys,useIsFullscreen,}from'@manapotion/react'constFullscreenButton=()=>{constisFullscreen=useIsFullscreen()return(<buttononClick={()=>{if(isFullscreen){exitFullscreen()unlockKeys()unlockOrientation()}else{enterFullscreen()lockOrientation('landscape')lockKeys(['Escape','KeyW','KeyA','KeyS','KeyD'])}}}>      Toggle fullscreen</button>)}

Note: Locking keys is aChrome experimental feature to maintain fullscreen when players press Esc (they have to hold it instead). It lets games show in-game dialogs that players can close with Esc without leaving fullscreen.

Tailwind

Mana Potion provides a Tailwind theme containing the followingscreens breakpoints:

  • 5xs: 192px
  • 4xs: 256px
  • 3xs: 320px
  • 2xs: 384px
  • xs: 512px
  • sm: 640px - Tailwind default
  • md: 768px - Tailwind default
  • lg: 1024px - Tailwind default
  • xl: 1280px - Tailwind default
  • xxl: 1536px - Tailwind default
  • 3xl: 1792px
  • 4xl: 2048px
  • 5xl: 2560px

Making games often involves supporting landscape mode on mobile devices, which require height media queries. The same values are used for the height media queries, but with a*-h suffix. So you can do:

  • xs-h:bg-red-500: Only for screens taller than 512px.
  • sm:xs-h:bg-red-500: Only for screens wider than 640px and taller than 512px.
  • sm:max-md:xs-h:max-sm-h:bg-red-500: Only between 640px to 768px wide and 512px to 640px high.

There is currently abug in Tailwind preventingmax-* classes from being generated when using non-pixel values including raw queries, which prevents us from having height media queries. This package contains a fix for this issue.

There are alsodesktop andmobile media queries that you can use to target mobile and desktop devices:

  • desktop:bg-red-500: Only for desktop devices.
  • mobile:bg-red-500: Only for mobile devices (includes tablets).

To add the theme to your Tailwind config:

/**@type {import('tailwindcss').Config} */import{tailwindThemeasmanapotionTheme}from'@manapotion/react'// or vue, svelte, vanillaexportdefault{content:['./index.html','./src/**/*.tsx'],theme:{screens:manapotionTheme.screens,extend:{screens:manapotionTheme.extend.screens,},},}

General tips

  • Clamp your device pixel ratio from 1 to 1.5. The sharpness of a DPR of 2 or more for high-density screens is not worth the performance hit (at least on mobile).
  • On mobile, clamp your frame rate to 60 FPS. It will prevent high-framerate devices from overheating and saves battery life.
  • If you use Three.js, somemath utilites such asclamp,lerp, andsmoothstep are included in Three.js

Community

Join theMana Potion Discord server.

Contributing

See thecontributing guide.

License

MIT

Author

Created by@verekia for 🔮MiniMana.io

Visit 🌐WebGameDev.com andjoin the Web Game Dev community.


[8]ページ先頭

©2009-2025 Movatter.jp