diff --git a/.github/workflows/main.yml b/.github/workflows/npm_publish.yml similarity index 62% rename from .github/workflows/main.yml rename to .github/workflows/npm_publish.yml index a14f438..df297d5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/npm_publish.yml @@ -13,9 +13,9 @@ jobs: - uses: actions/setup-node@v1 with: node-version: 12 - registry-url: https://npm.pkg.github.com/ - scope: '@ssethsara' + registry-url: https://registry.npmjs.org + scope: "@ssethsara" - run: npm install - - run: npm publish + - run: npm publish --access public env: - NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} + NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}} diff --git a/index.js b/index.js index 8183bfe..b1b55ca 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,3 @@ -export * from "./useNavMesh"; -export * from "./useYuka"; +export * from "./navmesh/useNavMesh"; +export * from "./navmesh/useYuka"; +export * from "./navmesh/NavMeshAgent"; diff --git a/navmesh/NavMeshAgent.jsx b/navmesh/NavMeshAgent.jsx new file mode 100644 index 0000000..c15c5af --- /dev/null +++ b/navmesh/NavMeshAgent.jsx @@ -0,0 +1,80 @@ +/* eslint-disable react/prop-types */ +import { BallCollider, CapsuleCollider, RigidBody } from "@react-three/rapier"; +import { useYuka } from "./useYuka"; +import { Vehicle } from "yuka"; +import getRandomArbitrary from "./RandomCalculations"; +import { Vector3 } from "three"; +import { useEffect, useRef } from "react"; +import { useNavMesh } from "./useNavMesh"; + +export function NavMeshAgent({ + name = "agent", + agentId = null, + position = [getRandomArbitrary(0, 60), 2, getRandomArbitrary(0, 60)], + navPoints = [new Vector3(10, 2, 10), new Vector3(60, 2, 60)], + maxSpeed = getRandomArbitrary(3, 10), + maxForce = getRandomArbitrary(30, 60), + isRandomNav = false, + removed = false, + isPlayerDetected = false, + collisionSize = 10, + capsuleColliderSize = [1, 1, 0.2], + ...props +}) { + const [refYuka] = useYuka({ + type: Vehicle, + name, + agentId, + position, + navPoints, + isRandomNav, + isPlayerDetected, + maxSpeed, + removed, + maxForce, + ...props, + }); + + const actions = useNavMesh((state) => state.actions); + const agentControl = useRef({ playerDetected: false }); + + // const Attacked = (event) => { + // if (event.rigidBodyObject.name == "Player") { + // console.log("Attacked"); + // } + // }; + + useEffect(() => { + return () => console.log("Destroyed", agentId); + }, []); + + return ( + + + {props.children} + + { + if ( + !agentControl.current.playerDetected && + object.rigidBodyObject.name == "Player" + ) { + actions.agentDetectPlayerTrigger(agentId, true); + } + }} + args={[collisionSize]} + position={[0, 0, 0]} + sensor + /> + + ); +} diff --git a/navmesh/RandomCalculations.jsx b/navmesh/RandomCalculations.jsx new file mode 100644 index 0000000..d220b47 --- /dev/null +++ b/navmesh/RandomCalculations.jsx @@ -0,0 +1,3 @@ +export default function getRandomArbitrary(min, max) { + return Math.random() * (max - min) + min; +} diff --git a/createConvexRegionHelper.jsx b/navmesh/createConvexRegionHelper.jsx similarity index 100% rename from createConvexRegionHelper.jsx rename to navmesh/createConvexRegionHelper.jsx diff --git a/navmesh/useNavMesh.jsx b/navmesh/useNavMesh.jsx new file mode 100644 index 0000000..faee8fa --- /dev/null +++ b/navmesh/useNavMesh.jsx @@ -0,0 +1,85 @@ +import * as THREE from "three"; +import { NavMeshLoader, Vector3 } from "yuka"; +import { create } from "zustand"; +import { createConvexRegionHelper } from "./createConvexRegionHelper"; + +export const useNavMesh = create((set, get) => ({ + navMesh: null, + intersects: new Vector3(), + agentList: [], + mutation: { + mouse: { x: 0, y: 0 }, + }, + level: { + geometry: new THREE.BufferGeometry(), + material: new THREE.MeshBasicMaterial(), + }, + actions: { + loadNavMesh(url) { + const loader = new NavMeshLoader(); + loader.load(url).then((navMesh) => { + const { geometry, material } = createConvexRegionHelper(navMesh); + set({ navMesh }); + set({ level: { geometry, material } }); + }); + }, + + setPosition(position) { + set({ intersects: position }); + }, + + setAgentList(agentList) { + set({ agentList: agentList }); + }, + + /** + * use to make agent follow player + */ + agentDetectPlayerTrigger(agentId, active) { + const { agentList } = get(); + const selectedAgent = agentList.current.find((agentData) => { + return agentData.agent.agentId === agentId; + }); + selectedAgent.agent.isPlayerDetected = active; + set({ agentList: agentList }); + }, + + /** + * use to stop agent from moving + */ + freezeAgentTrigger(agentId) { + const { agentList } = get(); + const selectedAgent = agentList.current.find((agentData) => { + return agentData.agent.agentId === agentId; + }); + selectedAgent.followPathBehavior.active = + !selectedAgent.followPathBehavior.active; + selectedAgent.onPathBehavior.active = + !selectedAgent.followPathBehavior.active; + + selectedAgent.agent.navRef.current.setEnabled(false); + selectedAgent.agent.velocity = new Vector3(0, 0, 0); + set({ agentList: agentList }); + }, + + /** + * use to remove agent from agent list + */ + removeAgent(agentId) { + const { agentList } = get(); + const selectedAgent = agentList.current.find((agentData) => { + return agentData.agent.agentId === agentId; + }); + if (selectedAgent) { + selectedAgent.followPathBehavior.active = false; + selectedAgent.onPathBehavior.active = false; + selectedAgent.agent.navRef.current.setEnabled(false); + selectedAgent.agent.velocity = new Vector3(0, 0, 0); + agentList.current = agentList.current.filter((agentData) => { + return agentData.agent.agentId !== agentId; + }); + } + set({ agentList: agentList }); + }, + }, +})); diff --git a/navmesh/useYuka.jsx b/navmesh/useYuka.jsx new file mode 100644 index 0000000..0178dea --- /dev/null +++ b/navmesh/useYuka.jsx @@ -0,0 +1,161 @@ +/* eslint-disable react/prop-types */ +import { useRef, useEffect, useState, useContext, createContext } from "react"; +import { + GameEntity, + EntityManager, + FollowPathBehavior, + OnPathBehavior, + Vector3, + ObstacleAvoidanceBehavior, + Smoother, +} from "yuka"; +import { useNavMesh } from "./useNavMesh"; +import { useFrame } from "@react-three/fiber"; +import getRandomArbitrary from "./RandomCalculations"; + +const context = createContext(); + +export function Manager({ children }) { + const [mgr] = useState(() => new EntityManager()); + const agentListRef = useRef([]); + const managerControl = useRef({ start: false }); + const navMesh = useNavMesh((state) => state.navMesh); + const agentList = useNavMesh((state) => state.agentList); + const actions = useNavMesh((state) => state.actions); + + useEffect(() => { + if (!navMesh) { + return; + } + const agents = mgr.entities.filter((item) => item.name === "Enemy"); + + agents.forEach((agent) => { + agent.boundingRadius = 1; + agent.smoother = new Smoother(20); + // Set up agent + const followPathBehavior = new FollowPathBehavior(); + const onPathBehavior = new OnPathBehavior(); + + //this avoid agent collide with each other + const obstaclesAvoidenceBehavior = new ObstacleAvoidanceBehavior(agents); + + obstaclesAvoidenceBehavior.active = false; + obstaclesAvoidenceBehavior.brakingWeight = 0.1; + obstaclesAvoidenceBehavior.weight = 5; + obstaclesAvoidenceBehavior.dBoxMinLength = 5; + followPathBehavior.active = false; + onPathBehavior.active = false; + onPathBehavior.radius = 1; + agent.steering.add(obstaclesAvoidenceBehavior); + agent.steering.add(followPathBehavior); + agent.steering.add(onPathBehavior); + + agentListRef.current.push({ + agent: agent, + followPathBehavior: followPathBehavior, + onPathBehavior: onPathBehavior, + obstaclesAvoidenceBehavior: obstaclesAvoidenceBehavior, + }); + }); + + actions.setAgentList(agentListRef); + + useNavMesh.subscribe((intersects) => findPathTo(intersects)); + + const setPaths = (agentDate, from, to) => { + const path = navMesh.findPath(from, to); + agentDate.onPathBehavior.path.clear(); + agentDate.followPathBehavior.path.clear(); + agentDate.onPathBehavior.active = true; + agentDate.followPathBehavior.active = true; + agentDate.obstaclesAvoidenceBehaviorw.active = true; + + for (const point of path) { + agentDate.followPathBehavior.path.add(point); + agentDate.onPathBehavior.path.add(point); + } + }; + + const findPathTo = (target) => { + target.agentList.current.forEach((agentDate) => { + const navPointsCount = agentDate.agent.navPoints.length; + if (agentDate.agent.isPlayerDetected) { + const from = agentDate.agent.position; + const to = new Vector3( + target.intersects.x, + target.intersects.y, + target.intersects.z + ); + setPaths(agentDate, from, to); + } else if (navPointsCount > 0) { + const pathData = setRoamingPath(agentDate.agent, navPointsCount); + setPaths(agentDate, pathData.from, pathData.to); + } + }); + }; + }, [navMesh]); + + const setRoamingPath = (agent, navPointsCount) => { + let currentPointIndex = agent.currentNavPoint; + const from = agent.position; + let to = agent.navPoints[currentPointIndex]; + if (from.distanceTo(to) < 0.5 && navPointsCount > 1) { + currentPointIndex = + currentPointIndex < navPointsCount - 1 ? currentPointIndex + 1 : 0; + to = agent.navPoints[currentPointIndex]; + agent.currentNavPoint = currentPointIndex; + } + const toVec3 = new Vector3(to.x, to.y, to.z); + return { from: from, to: toVec3 }; + }; + + useFrame((state, delta) => { + mgr.update(delta); + }); + + return {children}; +} + +export function useYuka({ + type = GameEntity, + agentId = null, + position = [getRandomArbitrary(0, 60), 2, getRandomArbitrary(0, 60)], + name = "unnamed", + navPoints = [], + isRandomNav = false, + isPlayerDetected = false, + maxForce, + removed, + maxSpeed, +}) { + // This hook makes set-up re-usable + const ref = useRef(); + const mgr = useContext(context); + const [entity] = useState(() => new type()); + useEffect(() => { + entity.position.set(...position); + entity.agentId = agentId; + entity.name = name; + entity.maxForce = maxForce; + entity.maxSpeed = maxSpeed; + entity.isRandomNav = isRandomNav; + entity.currentNavPoint = 0; + entity.isPlayerDetected = isPlayerDetected; + entity.navPoints = navPoints; + entity.navRef = ref; + entity.removed = removed; + entity.setRenderComponent(ref, (entity) => { + ref.current.setTranslation(entity.position); + ref.current.setRotation(entity.rotation); + }); + console.log("entity", entity); + mgr.add(entity); + return () => { + const removingEntity = mgr.entities.find( + (item) => item.agentId === entity.agentId + ); + mgr.remove(removingEntity); + }; + }, []); + return [ref, entity]; +} diff --git a/package.json b/package.json index 8f1fb2e..8dbd64e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@ssethsara/react-three-npc", - "version": "1.0.1", - "description": "", + "version": "1.2.0", + "description": "This is a non playerable character system for react three fiber based games. NPC will follow paths and target on navmesh. This system based on Yuka.js", "main": "index.js", "repository": { "url": "https://github.com/ssethsara/react-three-npc" diff --git a/useNavMesh.jsx b/useNavMesh.jsx deleted file mode 100644 index fd7faed..0000000 --- a/useNavMesh.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as THREE from "three"; -import { NavMeshLoader, Vector3 } from "yuka"; -import { create } from "zustand"; -import { createConvexRegionHelper } from "./createConvexRegionHelper"; - -export const useNavMesh = create((set, get) => ({ - raycaster: new THREE.Raycaster(), - camera: null, - viewport: new THREE.Vector2(window.innerWidth, window.innerHeight), - clock: null, - navMesh: null, - intersects: new Vector3(), - mutation: { - mouse: { x: 0, y: 0 }, - }, - refs: { - level: null, - pathHelper: null, - }, - level: { - geometry: new THREE.BufferGeometry(), - material: new THREE.MeshBasicMaterial(), - }, - actions: { - init(camera) { - set({ camera }); - }, - loadNavMesh(url) { - const loader = new NavMeshLoader(); - loader.load(url).then((navMesh) => { - const { geometry, material } = createConvexRegionHelper(navMesh); - set({ navMesh }); - set({ level: { geometry, material } }); - }); - }, - updateMouse({ clientX, clientY }) { - const { viewport, mutation } = get(); - - mutation.mouse.x = (clientX / viewport.x) * 2 - 1; - mutation.mouse.y = -(clientY / viewport.y) * 2 + 1; - }, - handleMouseDown() { - const { mutation, raycaster, camera, refs } = get(); - if (!refs.level) { - return null; - } - - raycaster.setFromCamera(mutation.mouse, camera); - - const intersects = raycaster.intersectObject(refs.level); - - if (intersects.length > 0) { - const point = new Vector3().copy(intersects[0].point); - set({ intersects: point }); - } - }, - setPosition(position) { - set({ intersects: position }); - }, - }, -})); diff --git a/useYuka.jsx b/useYuka.jsx deleted file mode 100644 index d9c6f0c..0000000 --- a/useYuka.jsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useRef, useEffect, useState, useContext, createContext } from "react"; -import { - GameEntity, - EntityManager, - FollowPathBehavior, - OnPathBehavior, - Vector3, -} from "yuka"; -import { useNavMesh } from "./useNavMesh"; -import { useFrame } from "@react-three/fiber"; - -const context = createContext(); - -function getRandomArbitrary(min, max) { - return Math.random() * (max - min) + min; -} - -export function Manager({ children }) { - const [mgr] = useState(() => new EntityManager()); - const playerList = useRef([]); - const navMesh = useNavMesh((state) => state.navMesh); - - useEffect(() => { - if (!navMesh) { - return; - } - - const players = mgr.entities.filter((item) => item.name === "Enemy"); - // const ghost = mgr.entities.find((item) => item.name === "Ghost"); - - players.forEach((player) => { - // Set up player - const followPathBehavior = new FollowPathBehavior(); - const onPathBehavior = new OnPathBehavior(); - player.maxSpeed = getRandomArbitrary(3, 10); - player.maxForce = getRandomArbitrary(30, 60); - followPathBehavior.active = false; - onPathBehavior.active = false; - onPathBehavior.radius = 1; - player.steering.add(followPathBehavior); - player.steering.add(onPathBehavior); - - playerList.current.push({ - player: player, - followPathBehavior: followPathBehavior, - onPathBehavior: onPathBehavior, - }); - }); - - useNavMesh.subscribe( - (intersects) => findPathTo(intersects), - (state) => state.intersects - ); - - function findPathTo(target) { - playerList.current.forEach((playerDate) => { - const from = playerDate.player.position; - const to = new Vector3( - target.intersects.x, - target.intersects.y, - target.intersects.z - ); - const path = navMesh.findPath(from, to); - - playerDate.onPathBehavior.active = true; - playerDate.onPathBehavior.path.clear(); - playerDate.followPathBehavior.active = true; - playerDate.followPathBehavior.path.clear(); - - for (const point of path) { - playerDate.followPathBehavior.path.add(point); - playerDate.onPathBehavior.path.add(point); - } - }); - } - }, [navMesh]); - - useFrame((state, delta) => mgr.update(delta)); - - return {children}; -} - -export function useYuka({ - type = GameEntity, - position = [getRandomArbitrary(0, 60), 1, getRandomArbitrary(0, 60)], - name = "unnamed", -}) { - // This hook makes set-up re-usable - const ref = useRef(); - const mgr = useContext(context); - const [entity] = useState(() => new type()); - useEffect(() => { - entity.position.set(...position); - entity.name = name; - entity.setRenderComponent(ref, (entity) => { - ref.current.setTranslation(entity.position); - ref.current.setRotation(entity.rotation); - // ref.current.position.copy(entity.position); - // ref.current.quaternion.copy(entity.rotation); - }); - mgr.add(entity); - return () => mgr.remove(entity); - }, []); - return [ref, entity]; -}