diff --git a/gameserver/config/index.ts b/gameserver/config/index.ts index 466151d..cbe7d86 100644 --- a/gameserver/config/index.ts +++ b/gameserver/config/index.ts @@ -22,12 +22,20 @@ export enum CoreErrorCodes { export const MOVABLE_UNIT_CONSTANTS = { MAX_STEER_FORCE: 10, - MAX_REPEL_FORCE: 70, + MAX_REPEL_FORCE: 30, - DESIRED_DIST_FROM_TARGET: 20, - ACCEPTABLE_DIST_FROM_EXPECTED_POS: 1, + MAX_DISTANCE_OFFSET_ALLOWED_FROM_EXPECTED_POSITION: 50, + + /** + * NEARBY_SEARCH_RADI serves following purpose + * 1. detect nearby allies under attack + * 2. find attack target + */ NEARBY_SEARCH_RADI: 150, - ENEMY_SEARCH_RADIUS: 200, - DESIRED_SEPERATION_DIST: 55, //to initiate repulsion force - MAX_TARGETPOS_OVERLAP_DIST: 50, + + /** + * + */ + MINIMUM_SEPERATION_DISTANCE_BETWEEN_UNITS: 30, //to initiate repulsion force + MAX_TARGETPOS_OVERLAP_DIST: 70, }; diff --git a/gameserver/core/Scene.ts b/gameserver/core/Scene.ts index e2bcb52..03075d8 100644 --- a/gameserver/core/Scene.ts +++ b/gameserver/core/Scene.ts @@ -39,7 +39,7 @@ export class Scene extends Quadtree { } /** - * Gets units which are within the bounding box(square) + * Gets units which are within the region * @param {*} soldier * @param {*} searchRadius * @returns @@ -49,17 +49,24 @@ export class Scene extends Quadtree { y: number, searchRadius: number, type?: SceneObjectType[] - ) { - let result = this.colliding({ x, y }, (a, b) => { - // a=> 1st arg, b => actual quadtree object - const aPos = new SAT.Vector(a.x, a.y); - const bPos = new SAT.Vector(b.x + b.width! / 2, b.y + b.height! / 2); - let distance = aPos.clone().sub(bPos).len(); - return distance <= 2 * searchRadius; +) { + let result = this.colliding({x, y}, (a, b) => { + // Create circles for each object + const aCircle = new SAT.Circle( + new SAT.Vector(a.x, a.y), + Math.max(searchRadius, a.r || 0), + ); + const bCircle = new SAT.Circle(new SAT.Vector(b.x, b.y), b.r); + + // Perform circle-circle collision detection + const response = new SAT.Response(); + const collided = SAT.testCircleCircle(aCircle, bCircle, response); + return collided; }); + if (type) result = result.filter((body) => type?.includes(body.type)); return result; - } +} //Check if unit/sceneItem is colliding with other units/soldiers checkCollisionOnObject( @@ -67,16 +74,13 @@ export class Scene extends Quadtree { callback: (arg0: SAT.Response, arg1: ISceneItem[]) => void ) { const mainCollidingObject = sceneItem.getSceneItem(); - //fetch all bodies which are colliding with the soldier specified by x,y,w,h in arg. - let collidingBodies = this.colliding({ - x: mainCollidingObject.pos.x, - y: mainCollidingObject.pos.y, - width: mainCollidingObject.w, - height: mainCollidingObject.h, - }); - + //fetch all bodies which are colliding with the soldier specified by x,y,r in arg. + let collidingBodies = this.getNearbyUnits( + mainCollidingObject.pos.x, + mainCollidingObject.pos.y, + mainCollidingObject.r + ); //Colliding Bodies will always have 1 element, which is the soldier itself. - if (collidingBodies.length < 2) return; //Obtain "SAT.Response" for each collision. const satBoxPolygons = collidingBodies @@ -92,9 +96,9 @@ export class Scene extends Quadtree { } const res = new SAT.Response(); - const isColliding = SAT.testPolygonPolygon( - mainCollidingObject.toPolygon(), - collidingBody.getSceneItem().toPolygon(), + const isColliding = SAT.testCircleCircle( + mainCollidingObject, + collidingBody.getSceneItem(), res ); if (!isColliding) return; diff --git a/gameserver/core/types/SceneObject.ts b/gameserver/core/types/SceneObject.ts index 2c608ec..e6eba58 100644 --- a/gameserver/core/types/SceneObject.ts +++ b/gameserver/core/types/SceneObject.ts @@ -4,30 +4,26 @@ import SAT from "sat"; * @classdesc Any object that is meant to be part of the Scene should extend this class. */ export type SceneObjectType = "FIXED" | "MOVABLE"; -export class SceneObject extends SAT.Box { +export class SceneObject extends SAT.Circle { id: string; x: number; y: number; - width: number; - height: number; + r: number; type: SceneObjectType; collidable: boolean; constructor( id: string, x: number, y: number, - width = 35, - height = 35, + radius = 35, type: SceneObjectType, collidable: boolean = true ) { - // {pos:{x,y}} - super(new SAT.Vector(x, y), width, height); + super(new SAT.Vector(x, y), radius); this.id = id; this.x = this.pos.x; this.y = this.pos.y; - this.width = this.w; - this.height = this.h; + this.r = radius; this.type = type; this.collidable = collidable; } diff --git a/gameserver/core/types/TypeQuadtreeItem.ts b/gameserver/core/types/TypeQuadtreeItem.ts index 8a93dbb..7be9419 100644 --- a/gameserver/core/types/TypeQuadtreeItem.ts +++ b/gameserver/core/types/TypeQuadtreeItem.ts @@ -3,8 +3,7 @@ import { SceneObjectType } from "./SceneObject"; export type TypeQuadtreeItem = { x: Quadtree.QuadtreeItem["x"]; y: Quadtree.QuadtreeItem["y"]; - width?: Quadtree.QuadtreeItem["width"]; - height?: Quadtree.QuadtreeItem["height"]; + r?: number; id: string; type: SceneObjectType; collidable: boolean; diff --git a/gameserver/schema/PlayerState.ts b/gameserver/schema/PlayerState.ts index 5da6843..40c8da4 100644 --- a/gameserver/schema/PlayerState.ts +++ b/gameserver/schema/PlayerState.ts @@ -65,7 +65,6 @@ export class PlayerState extends Schema implements ISceneItem { x, y, 100, - 100, "FIXED", false ); diff --git a/gameserver/schema/SoldierState.ts b/gameserver/schema/SoldierState.ts index 70afd5f..7460404 100644 --- a/gameserver/schema/SoldierState.ts +++ b/gameserver/schema/SoldierState.ts @@ -11,7 +11,7 @@ import { MOVABLE_UNIT_CONSTANTS } from "../config"; import { GameStateManagerType } from "./PlayerState"; import { ISceneItem } from "../core/types/ISceneItem"; import { TypeQuadtreeItem } from "../core/types/TypeQuadtreeItem"; -import { IBoidAgent } from '../core/types/IBoidAgent'; +import { IBoidAgent } from "../core/types/IBoidAgent"; function mapRange( val: number, @@ -26,10 +26,7 @@ function mapRange( return targetRangeStart + normalizedGivenRange * targetRange; } -export class SoldierState - extends Schema - implements ISceneItem, IBoidAgent -{ +export class SoldierState extends Schema implements ISceneItem, IBoidAgent { @type("number") currentPositionX: number = 0; @type("number") currentPositionY: number = 0; @@ -43,8 +40,7 @@ export class SoldierState @type("string") type: SoldierType = "SPEARMAN"; - @type("number") width: number = 32; - @type("number") height: number = 32; + @type("number") radius: number = 32; @type("number") health: number = 100; @type("number") speed: number; @@ -104,7 +100,7 @@ export class SoldierState this.damage = SoldierTypeConfig[this.type].damage; this.cost = SoldierTypeConfig[this.type].cost; - this.sceneItemRef = new SceneObject(this.id, x, y, 32, 32, "MOVABLE", true); + this.sceneItemRef = new SceneObject(this.id, x, y, 32, "MOVABLE", true); } setGroupLeaderId(leaderId: string) { @@ -119,7 +115,7 @@ export class SoldierState } getExpectedPosition() { - return new SAT.Vector(this.expectedPositionX + this.offsetFromPosition.x, this.expectedPositionY + this.offsetFromPosition.y); + return new SAT.Vector(this.expectedPositionX, this.expectedPositionY); } setAttackTarget(target: SoldierState | null) { @@ -149,7 +145,7 @@ export class SoldierState let distanceToExpectedPos = expectedPos.sub(this.getSceneItem().pos).len(); if ( distanceToExpectedPos <= - MOVABLE_UNIT_CONSTANTS.ACCEPTABLE_DIST_FROM_EXPECTED_POS + MOVABLE_UNIT_CONSTANTS.MAX_DISTANCE_OFFSET_ALLOWED_FROM_EXPECTED_POSITION ) { this.isAtDestination = true; } else this.isAtDestination = false; @@ -159,7 +155,8 @@ export class SoldierState hasReachedDestination() { let distanceToExpectedPos = this.getDistanceFromExpectedPosition(); this.isAtDestination = - distanceToExpectedPos <= MOVABLE_UNIT_CONSTANTS.DESIRED_DIST_FROM_TARGET; + distanceToExpectedPos <= + MOVABLE_UNIT_CONSTANTS.MAX_DISTANCE_OFFSET_ALLOWED_FROM_EXPECTED_POSITION; return this.isAtDestination; } @@ -192,7 +189,7 @@ export class SoldierState if ( distanceFromExpectedPosition <= - MOVABLE_UNIT_CONSTANTS.DESIRED_DIST_FROM_TARGET + MOVABLE_UNIT_CONSTANTS.MAX_DISTANCE_OFFSET_ALLOWED_FROM_EXPECTED_POSITION ) { desiredVector.scale(0); } else desiredVector.normalize().scale(this.speed); @@ -210,8 +207,8 @@ export class SoldierState ) { const soldier = this.getSceneItem(); const nearbyUnits = stateManager.scene.getNearbyUnits( - soldier.x + soldier.w / 2, - soldier.y + soldier.h / 2, + soldier.x + soldier.r / 2, + soldier.y + soldier.r / 2, MOVABLE_UNIT_CONSTANTS.NEARBY_SEARCH_RADI, ["MOVABLE"] ); @@ -247,7 +244,8 @@ export class SoldierState .len(); const unitSeperationBetweenCertainThreshold = - distanceBetweenUnits <= MOVABLE_UNIT_CONSTANTS.DESIRED_SEPERATION_DIST; + distanceBetweenUnits <= + MOVABLE_UNIT_CONSTANTS.MINIMUM_SEPERATION_DISTANCE_BETWEEN_UNITS; if (!unitSeperationBetweenCertainThreshold) return; @@ -296,34 +294,14 @@ export class SoldierState tick(delta: number, stateManager: GameStateManagerType) { this.currentState = this.stateMachine.currentState as any; - const soldier = this.getSceneItem(); - - // if group-leader not present anymore, ensure offset is 0 - if ( - !stateManager.getPlayer(this.playerId)?.getSoldier(this.groupLeaderId) - ) { + // in case group-leader instance not found, we reset offset + const groupLeaderRef = stateManager + .getPlayer(this.playerId) + ?.getSoldier(this.groupLeaderId); + if (!groupLeaderRef) { this.groupLeaderId = null; this.offsetFromPosition = new SAT.Vector(0, 0); - } - - const frictionForce = this.velocityVector.clone().scale(-1.0 * delta); - this.velocityVector.add(frictionForce); - const steerForce = this.getSteerVector(this.getExpectedPosition()); - let seperationForce = this.getSeperationVector( - stateManager, - (a: SoldierState, b: SoldierState) => { - return a.hasReachedDestination() && b.hasReachedDestination(); - } - ); - - const netForce = steerForce.clone().add(seperationForce); - this.applyForce(netForce); - - this.velocityVector.normalize().scale(this.speed * delta); - const newPosition = soldier.pos.clone().add(this.getVelocityVector()); - - this.setPosition(newPosition); - + } stateManager.scene.checkCollisionOnObject( this, ( @@ -370,4 +348,21 @@ export class SoldierState this.stateMachine.tick({ delta, stateManager, soldier: this }); } + + move(delta: number, stateManager: GameStateManagerType) { + const steerForce = this.getSteerVector(this.getExpectedPosition()); + const seperationForce = this.getSeperationVector( + stateManager, + (a: SoldierState, b: SoldierState) => { + return a.hasReachedDestination() && b.hasReachedDestination(); + } + ); + const netForce = steerForce.clone().add(seperationForce); + this.applyForce(netForce); + this.velocityVector.normalize().scale(this.speed * delta); + const newPosition = this.getSceneItem()! + .pos.clone() + .add(this.getVelocityVector()); + this.setPosition(newPosition); + } } diff --git a/gameserver/stateMachines/soldier-state-machine/SoldierStateBehaviour.ts b/gameserver/stateMachines/soldier-state-machine/SoldierStateBehaviour.ts index 688a185..b282478 100644 --- a/gameserver/stateMachines/soldier-state-machine/SoldierStateBehaviour.ts +++ b/gameserver/stateMachines/soldier-state-machine/SoldierStateBehaviour.ts @@ -1,4 +1,4 @@ -import SoldierConstants from "../../unitConstants"; +import { MOVABLE_UNIT_CONSTANTS } from "../../config"; import SAT from "sat"; import { IStateActions } from "../../core/CustomStateMachine"; import { AllianceTypes } from "../../AllianceTracker"; @@ -22,9 +22,10 @@ export default { // if nearby unit getting attacked. const nearbyUnits = stateManager.scene.getNearbyUnits( - soldier.getSceneItem().pos.x + soldier.getSceneItem().w / 2, - soldier.getSceneItem().pos.y + soldier.getSceneItem().h / 2, - SoldierConstants.NEARBY_SEARCH_RADI + soldier.getSceneItem().pos.x, + soldier.getSceneItem().pos.y, + MOVABLE_UNIT_CONSTANTS.NEARBY_SEARCH_RADI, + ['MOVABLE'] ); if (nearbyUnits.length < 2) return; @@ -68,12 +69,12 @@ export default { soldier.stateMachine.controller.send("ReachedPosition"); stateMachineTrigged = true; } - + soldier.move(delta, stateManager); const nearbyUnits = stateManager.scene.getNearbyUnits( - soldier.getSceneItem().pos.x + soldier.getSceneItem().w / 2, - soldier.getSceneItem().pos.y + soldier.getSceneItem().h / 2, - SoldierConstants.NEARBY_SEARCH_RADI, - ['MOVABLE'] + soldier.getSceneItem().pos.x, + soldier.getSceneItem().pos.y, + MOVABLE_UNIT_CONSTANTS.NEARBY_SEARCH_RADI, + ["MOVABLE"] ); if (nearbyUnits.length < 2) return; @@ -81,14 +82,15 @@ export default { nearbyUnits.forEach((unit) => { if (unit.id === soldier.id) return; - const nearbySoldierUnit = stateManager.scene.getSceneItemById(unit.id); + const nearbySoldierUnit = + stateManager.scene.getSceneItemById(unit.id); if (!nearbySoldierUnit) return; // if nearby unit (of same team) has same destination (approx.) let overlapExpectedPos = new SAT.Vector() .copy(nearbySoldierUnit.getExpectedPosition()) .sub(soldier.getExpectedPosition()) - .len() <= SoldierConstants.MAX_TARGETPOS_OVERLAP_DIST; + .len() <= MOVABLE_UNIT_CONSTANTS.MAX_TARGETPOS_OVERLAP_DIST; let anyOneAtDest = nearbySoldierUnit.hasReachedDestination() || @@ -121,7 +123,7 @@ export default { .copy(attackTarget.getSceneItem().pos) .sub(soldier.getSceneItem().pos) .len(); - if (distToTarget > SoldierConstants.DESIRED_DIST_FROM_TARGET) { + if (distToTarget > MOVABLE_UNIT_CONSTANTS.NEARBY_SEARCH_RADI) { soldier.stateMachine.controller.send("TargetNotInRange"); return; } @@ -153,10 +155,10 @@ export default { try { soldier.setAttackTarget(null); var nearbyUnits = stateManager.scene.getNearbyUnits( - soldier.getSceneItem().pos.x + soldier.getSceneItem().w / 2, - soldier.getSceneItem().pos.y + soldier.getSceneItem().h / 2, - SoldierConstants.ENEMY_SEARCH_RADIUS, - ['MOVABLE'] + soldier.getSceneItem().pos.x, + soldier.getSceneItem().pos.y, + MOVABLE_UNIT_CONSTANTS.NEARBY_SEARCH_RADI, + ["MOVABLE"] ); if (nearbyUnits.length < 2) { throw new Error( @@ -168,7 +170,9 @@ export default { let minDist = Number.POSITIVE_INFINITY; let nearestUnit: SoldierState | null = null; for (const unit of nearbyUnits) { - let unitSoldier = stateManager.scene.getSceneItemById(unit.id); + let unitSoldier = stateManager.scene.getSceneItemById( + unit.id + ); if ( !unitSoldier || unit.id === soldier.id || @@ -245,11 +249,12 @@ export default { soldier.expectedPositionX = soldierAttackTarget.getSceneItem().pos.x; soldier.expectedPositionY = soldierAttackTarget.getSceneItem().pos.y; + soldier.move(delta, stateManager); const distToTarget = new SAT.Vector() .copy(soldierAttackTarget.getSceneItem().pos) .sub(soldier.getSceneItem().pos) .len(); - if (distToTarget <= SoldierConstants.DESIRED_DIST_FROM_TARGET) { + if (distToTarget <= MOVABLE_UNIT_CONSTANTS.NEARBY_SEARCH_RADI) { soldier.stateMachine.controller.send("TargetInRange"); } } catch (err) { diff --git a/gameserver/unitConstants.ts b/gameserver/unitConstants.ts deleted file mode 100644 index ad62601..0000000 --- a/gameserver/unitConstants.ts +++ /dev/null @@ -1,10 +0,0 @@ -export default { - MAX_STEER_FORCE: 5, - MAX_REPEL_FORCE: 9, - DESIRED_DIST_FROM_TARGET: 50, - ACCEPTABLE_DIST_FROM_EXPECTED_POS: 2, - NEARBY_SEARCH_RADI: 150, - ENEMY_SEARCH_RADIUS: 200, - DESIRED_SEPERATION_DIST: 20, //to initiate repulsion force - MAX_TARGETPOS_OVERLAP_DIST: 40, -}; diff --git a/package.json b/package.json index ebdac97..5506127 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.ts", "scripts": { - "build": "webpack --config webpack.config.js --watch", + "build": "webpack --watch --config webpack.config.js", "start": "tsx watch gameserver/index", "dev": "webpack --config webpack.config.js --watch && tsx watch gameserver/index.ts", "deploy": "npx webpack --config webpack.config.js && tsx gameserver/index", diff --git a/public/scenes/GameScene.ts b/public/scenes/GameScene.ts index 33fb2d4..bacee3f 100644 --- a/public/scenes/GameScene.ts +++ b/public/scenes/GameScene.ts @@ -1,5 +1,4 @@ import { PacketType } from "../../common/PacketType"; -import { SoldierType } from "../../common/SoldierType"; import { PlayerState } from "../../gameserver/schema/PlayerState"; import { SoldierState } from "../../gameserver/schema/SoldierState"; import { NetworkManager } from "../NetworkManager"; @@ -550,20 +549,18 @@ export class GameScene extends BaseScene { ); playerState?.soldiers.forEach((value) => { painter.lineStyle(1, 0x00ffee, 1); - painter.strokeRect( - value.currentPositionX! - value.width/2, - value.currentPositionY! - value.height/2, - value.width!, - value.height! + painter.strokeCircle( + value.currentPositionX!, + value.currentPositionY!, + value.radius!, ); - painter.strokeRect( - value.expectedPositionX! - value.width/2, - value.expectedPositionY! - value.height/2, - value.width!, - value.height! + painter.strokeCircle( + value.expectedPositionX!, + value.expectedPositionY!, + value.radius!, ); - painter.lineStyle(1, 0x00ffee, 1); + painter.lineStyle(1, 0xffffee, 1); painter.strokeLineShape( new Phaser.Geom.Line( value.currentPositionX!,