Skip to content

Commit

Permalink
Structured the npc system
Browse files Browse the repository at this point in the history
  • Loading branch information
Supun-ascentic committed Feb 16, 2024
1 parent 4bd687b commit 1810052
Show file tree
Hide file tree
Showing 10 changed files with 338 additions and 174 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
5 changes: 3 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./useNavMesh";
export * from "./useYuka";
export * from "./navmesh/useNavMesh";
export * from "./navmesh/useYuka";
export * from "./navmesh/NavMeshAgent";
80 changes: 80 additions & 0 deletions navmesh/NavMeshAgent.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<RigidBody
ref={refYuka}
colliders={false}
linearDamping={0}
type="kinematicPosition"
agentId={agentId}
position={position}
name="Enemy"
lockRotations
>
<CapsuleCollider args={capsuleColliderSize} position={[0, 0, 0]}>
{props.children}
</CapsuleCollider>
<BallCollider
onIntersectionEnter={(object) => {
if (
!agentControl.current.playerDetected &&
object.rigidBodyObject.name == "Player"
) {
actions.agentDetectPlayerTrigger(agentId, true);
}
}}
args={[collisionSize]}
position={[0, 0, 0]}
sensor
/>
</RigidBody>
);
}
3 changes: 3 additions & 0 deletions navmesh/RandomCalculations.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
File renamed without changes.
85 changes: 85 additions & 0 deletions navmesh/useNavMesh.jsx
Original file line number Diff line number Diff line change
@@ -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 });
},
},
}));
161 changes: 161 additions & 0 deletions navmesh/useYuka.jsx
Original file line number Diff line number Diff line change
@@ -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 <context.Provider value={mgr}>{children}</context.Provider>;
}

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];
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Loading

0 comments on commit 1810052

Please sign in to comment.