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];
-}