feat: 寻路优化
This commit is contained in:
@@ -1472,7 +1472,7 @@
|
|||||||
"_enabled": true,
|
"_enabled": true,
|
||||||
"__prefab": null,
|
"__prefab": null,
|
||||||
"tag": 0,
|
"tag": 0,
|
||||||
"_group": 1,
|
"_group": 2,
|
||||||
"_density": 1,
|
"_density": 1,
|
||||||
"_sensor": false,
|
"_sensor": false,
|
||||||
"_friction": 0.2,
|
"_friction": 0.2,
|
||||||
@@ -1912,8 +1912,6 @@
|
|||||||
"moveSpeed": 300,
|
"moveSpeed": 300,
|
||||||
"mapWidth": 1080,
|
"mapWidth": 1080,
|
||||||
"mapHeight": 1920,
|
"mapHeight": 1920,
|
||||||
"attackPreferredDistance": 220,
|
|
||||||
"attackDistanceTolerance": 10,
|
|
||||||
"_id": "c1AuAU3IlKnLOzgk9vsBr4"
|
"_id": "c1AuAU3IlKnLOzgk9vsBr4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -3696,7 +3694,7 @@
|
|||||||
"_enabled": true,
|
"_enabled": true,
|
||||||
"__prefab": null,
|
"__prefab": null,
|
||||||
"tag": 0,
|
"tag": 0,
|
||||||
"_group": 1,
|
"_group": 4,
|
||||||
"_density": 1,
|
"_density": 1,
|
||||||
"_sensor": false,
|
"_sensor": false,
|
||||||
"_friction": 0.2,
|
"_friction": 0.2,
|
||||||
@@ -3896,7 +3894,7 @@
|
|||||||
},
|
},
|
||||||
"_lscale": {
|
"_lscale": {
|
||||||
"__type__": "cc.Vec3",
|
"__type__": "cc.Vec3",
|
||||||
"x": 4.172,
|
"x": -4.172,
|
||||||
"y": 1.768,
|
"y": 1.768,
|
||||||
"z": 2.116
|
"z": 2.116
|
||||||
},
|
},
|
||||||
@@ -4330,20 +4328,20 @@
|
|||||||
"_enabled": true,
|
"_enabled": true,
|
||||||
"__prefab": null,
|
"__prefab": null,
|
||||||
"tag": 0,
|
"tag": 0,
|
||||||
"_group": 1,
|
"_group": 4,
|
||||||
"_density": 1,
|
"_density": 1,
|
||||||
"_sensor": false,
|
"_sensor": false,
|
||||||
"_friction": 0.2,
|
"_friction": 0.2,
|
||||||
"_restitution": 0,
|
"_restitution": 0,
|
||||||
"_offset": {
|
"_offset": {
|
||||||
"__type__": "cc.Vec2",
|
"__type__": "cc.Vec2",
|
||||||
"x": -16.1,
|
"x": -11.1,
|
||||||
"y": 7.4
|
"y": -0.2
|
||||||
},
|
},
|
||||||
"_size": {
|
"_size": {
|
||||||
"__type__": "cc.Size",
|
"__type__": "cc.Size",
|
||||||
"width": 97.4,
|
"width": 86.2,
|
||||||
"height": 121.5
|
"height": 110.3
|
||||||
},
|
},
|
||||||
"_id": "3ca3euVMhBkrOMELnKR9Zg"
|
"_id": "3ca3euVMhBkrOMELnKR9Zg"
|
||||||
},
|
},
|
||||||
@@ -4964,20 +4962,20 @@
|
|||||||
"_enabled": true,
|
"_enabled": true,
|
||||||
"__prefab": null,
|
"__prefab": null,
|
||||||
"tag": 0,
|
"tag": 0,
|
||||||
"_group": 1,
|
"_group": 4,
|
||||||
"_density": 1,
|
"_density": 1,
|
||||||
"_sensor": false,
|
"_sensor": false,
|
||||||
"_friction": 0.2,
|
"_friction": 0.2,
|
||||||
"_restitution": 0,
|
"_restitution": 0,
|
||||||
"_offset": {
|
"_offset": {
|
||||||
"__type__": "cc.Vec2",
|
"__type__": "cc.Vec2",
|
||||||
"x": 2.3,
|
"x": 1.7,
|
||||||
"y": -0.3
|
"y": -3.3
|
||||||
},
|
},
|
||||||
"_size": {
|
"_size": {
|
||||||
"__type__": "cc.Size",
|
"__type__": "cc.Size",
|
||||||
"width": 74.8,
|
"width": 45.7,
|
||||||
"height": 132
|
"height": 119.5
|
||||||
},
|
},
|
||||||
"_id": "efpC9N9EpG+r7WSMK95lgW"
|
"_id": "efpC9N9EpG+r7WSMK95lgW"
|
||||||
},
|
},
|
||||||
@@ -5371,20 +5369,20 @@
|
|||||||
"_enabled": true,
|
"_enabled": true,
|
||||||
"__prefab": null,
|
"__prefab": null,
|
||||||
"tag": 0,
|
"tag": 0,
|
||||||
"_group": 1,
|
"_group": 4,
|
||||||
"_density": 1,
|
"_density": 1,
|
||||||
"_sensor": false,
|
"_sensor": false,
|
||||||
"_friction": 0.2,
|
"_friction": 0.2,
|
||||||
"_restitution": 0,
|
"_restitution": 0,
|
||||||
"_offset": {
|
"_offset": {
|
||||||
"__type__": "cc.Vec2",
|
"__type__": "cc.Vec2",
|
||||||
"x": 1.3,
|
"x": 6,
|
||||||
"y": 7.5
|
"y": 3.2
|
||||||
},
|
},
|
||||||
"_size": {
|
"_size": {
|
||||||
"__type__": "cc.Size",
|
"__type__": "cc.Size",
|
||||||
"width": 88.9,
|
"width": 73.1,
|
||||||
"height": 96.6
|
"height": 65.6
|
||||||
},
|
},
|
||||||
"_id": "3dz7NavQFBMZ60R0LoZHEP"
|
"_id": "3dz7NavQFBMZ60R0LoZHEP"
|
||||||
},
|
},
|
||||||
@@ -5778,7 +5776,7 @@
|
|||||||
"_enabled": true,
|
"_enabled": true,
|
||||||
"__prefab": null,
|
"__prefab": null,
|
||||||
"tag": 0,
|
"tag": 0,
|
||||||
"_group": 1,
|
"_group": 4,
|
||||||
"_density": 1,
|
"_density": 1,
|
||||||
"_sensor": false,
|
"_sensor": false,
|
||||||
"_friction": 0.2,
|
"_friction": 0.2,
|
||||||
@@ -6412,7 +6410,7 @@
|
|||||||
"_enabled": true,
|
"_enabled": true,
|
||||||
"__prefab": null,
|
"__prefab": null,
|
||||||
"tag": 0,
|
"tag": 0,
|
||||||
"_group": 1,
|
"_group": 4,
|
||||||
"_density": 1,
|
"_density": 1,
|
||||||
"_sensor": false,
|
"_sensor": false,
|
||||||
"_friction": 0.2,
|
"_friction": 0.2,
|
||||||
@@ -6612,7 +6610,7 @@
|
|||||||
},
|
},
|
||||||
"_lscale": {
|
"_lscale": {
|
||||||
"__type__": "cc.Vec3",
|
"__type__": "cc.Vec3",
|
||||||
"x": 3.256,
|
"x": -3.256,
|
||||||
"y": 1.763,
|
"y": 1.763,
|
||||||
"z": 1.763
|
"z": 1.763
|
||||||
},
|
},
|
||||||
@@ -7046,7 +7044,7 @@
|
|||||||
"_enabled": true,
|
"_enabled": true,
|
||||||
"__prefab": null,
|
"__prefab": null,
|
||||||
"tag": 0,
|
"tag": 0,
|
||||||
"_group": 1,
|
"_group": 4,
|
||||||
"_density": 1,
|
"_density": 1,
|
||||||
"_sensor": false,
|
"_sensor": false,
|
||||||
"_friction": 0.2,
|
"_friction": 0.2,
|
||||||
@@ -7246,7 +7244,7 @@
|
|||||||
},
|
},
|
||||||
"_lscale": {
|
"_lscale": {
|
||||||
"__type__": "cc.Vec3",
|
"__type__": "cc.Vec3",
|
||||||
"x": 1.36,
|
"x": 1.86,
|
||||||
"y": 0.822,
|
"y": 0.822,
|
||||||
"z": 1.154
|
"z": 1.154
|
||||||
},
|
},
|
||||||
@@ -7680,7 +7678,7 @@
|
|||||||
"_enabled": true,
|
"_enabled": true,
|
||||||
"__prefab": null,
|
"__prefab": null,
|
||||||
"tag": 0,
|
"tag": 0,
|
||||||
"_group": 1,
|
"_group": 4,
|
||||||
"_density": 1,
|
"_density": 1,
|
||||||
"_sensor": false,
|
"_sensor": false,
|
||||||
"_friction": 0.2,
|
"_friction": 0.2,
|
||||||
@@ -8314,7 +8312,7 @@
|
|||||||
"_enabled": true,
|
"_enabled": true,
|
||||||
"__prefab": null,
|
"__prefab": null,
|
||||||
"tag": 0,
|
"tag": 0,
|
||||||
"_group": 1,
|
"_group": 4,
|
||||||
"_density": 1,
|
"_density": 1,
|
||||||
"_sensor": false,
|
"_sensor": false,
|
||||||
"_friction": 0.2,
|
"_friction": 0.2,
|
||||||
|
|||||||
@@ -3,223 +3,246 @@ const { ccclass, property } = _decorator;
|
|||||||
|
|
||||||
// A*寻路节点
|
// A*寻路节点
|
||||||
export class PathNode {
|
export class PathNode {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
gCost: number = 0; // 从起点到当前点的实际代价
|
gCost: number = 0; // 从起点到当前点的实际代价
|
||||||
hCost: number = 0; // 从当前点到终点的启发式代价
|
hCost: number = 0; // 从当前点到终点的启发式代价
|
||||||
fCost: number = 0; // 总代价 f = g + h
|
fCost: number = 0; // 总代价 f = g + h
|
||||||
parent: PathNode | null = null;
|
parent: PathNode | null = null;
|
||||||
walkable: boolean = true;
|
walkable: boolean = true;
|
||||||
|
moveCost: number = 1.0; // 从父节点移动到此节点的代价
|
||||||
|
|
||||||
constructor(x: number, y: number, walkable: boolean = true) {
|
constructor(x: number, y: number, walkable: boolean = true) {
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
this.walkable = walkable;
|
this.walkable = walkable;
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateFCost() {
|
calculateFCost() {
|
||||||
this.fCost = this.gCost + this.hCost;
|
this.fCost = this.gCost + this.hCost;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ccclass('AStarPathfinding')
|
@ccclass('AStarPathfinding')
|
||||||
export class AStarPathfinding extends Component {
|
export class AStarPathfinding extends Component {
|
||||||
|
|
||||||
private grid: PathNode[][] = [];
|
private grid: PathNode[][] = [];
|
||||||
private gridWidth: number = 0;
|
private gridWidth: number = 0;
|
||||||
private gridHeight: number = 0;
|
private gridHeight: number = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化寻路网格
|
* 初始化寻路网格
|
||||||
* @param width 网格宽度
|
* @param width 网格宽度
|
||||||
* @param height 网格高度
|
* @param height 网格高度
|
||||||
* @param walkableData 可行走数据,0表示不可行走,1表示可行走
|
* @param walkableData 可行走数据,0表示不可行走,1表示可行走
|
||||||
*/
|
*/
|
||||||
initializeGrid(width: number, height: number, walkableData: number[][]) {
|
initializeGrid(width: number, height: number, walkableData: number[][]) {
|
||||||
this.gridWidth = width;
|
this.gridWidth = width;
|
||||||
this.gridHeight = height;
|
this.gridHeight = height;
|
||||||
this.grid = [];
|
this.grid = [];
|
||||||
|
|
||||||
for (let x = 0; x < width; x++) {
|
for (let x = 0; x < width; x++) {
|
||||||
this.grid[x] = [];
|
this.grid[x] = [];
|
||||||
for (let y = 0; y < height; y++) {
|
for (let y = 0; y < height; y++) {
|
||||||
const walkable = walkableData[y] && walkableData[y][x] === 1;
|
const walkable = walkableData[y] && walkableData[y][x] === 1;
|
||||||
this.grid[x][y] = new PathNode(x, y, walkable);
|
this.grid[x][y] = new PathNode(x, y, walkable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用A*算法寻找路径
|
||||||
|
* @param startX 起点X坐标
|
||||||
|
* @param startY 起点Y坐标
|
||||||
|
* @param targetX 终点X坐标
|
||||||
|
* @param targetY 终点Y坐标
|
||||||
|
* @returns 路径点数组,如果找不到路径返回空数组
|
||||||
|
*/
|
||||||
|
findPath(startX: number, startY: number, targetX: number, targetY: number): Vec2[] {
|
||||||
|
// 验证起点和终点是否有效
|
||||||
|
if (!this.isValidPosition(startX, startY) || !this.isValidPosition(targetX, targetY)) {
|
||||||
|
console.warn('起点或终点坐标无效');
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
if (!this.grid[startX][startY].walkable || !this.grid[targetX][targetY].walkable) {
|
||||||
* 使用A*算法寻找路径
|
console.warn('起点或终点不可行走');
|
||||||
* @param startX 起点X坐标
|
return [];
|
||||||
* @param startY 起点Y坐标
|
|
||||||
* @param targetX 终点X坐标
|
|
||||||
* @param targetY 终点Y坐标
|
|
||||||
* @returns 路径点数组,如果找不到路径返回空数组
|
|
||||||
*/
|
|
||||||
findPath(startX: number, startY: number, targetX: number, targetY: number): Vec2[] {
|
|
||||||
// 验证起点和终点是否有效
|
|
||||||
if (!this.isValidPosition(startX, startY) || !this.isValidPosition(targetX, targetY)) {
|
|
||||||
console.warn('起点或终点坐标无效');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.grid[startX][startY].walkable || !this.grid[targetX][targetY].walkable) {
|
|
||||||
console.warn('起点或终点不可行走');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startX === targetX && startY === targetY) {
|
|
||||||
return [new Vec2(startX, startY)];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置所有节点
|
|
||||||
this.resetNodes();
|
|
||||||
|
|
||||||
const startNode = this.grid[startX][startY];
|
|
||||||
const targetNode = this.grid[targetX][targetY];
|
|
||||||
|
|
||||||
const openSet: PathNode[] = [];
|
|
||||||
const closedSet: PathNode[] = [];
|
|
||||||
|
|
||||||
openSet.push(startNode);
|
|
||||||
|
|
||||||
while (openSet.length > 0) {
|
|
||||||
// 找到f值最小的节点
|
|
||||||
let currentNode = openSet[0];
|
|
||||||
for (let i = 1; i < openSet.length; i++) {
|
|
||||||
if (openSet[i].fCost < currentNode.fCost ||
|
|
||||||
(openSet[i].fCost === currentNode.fCost && openSet[i].hCost < currentNode.hCost)) {
|
|
||||||
currentNode = openSet[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从开放列表移除当前节点,添加到关闭列表
|
|
||||||
openSet.splice(openSet.indexOf(currentNode), 1);
|
|
||||||
closedSet.push(currentNode);
|
|
||||||
|
|
||||||
// 如果到达目标节点,重建路径
|
|
||||||
if (currentNode === targetNode) {
|
|
||||||
return this.retracePath(startNode, targetNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查相邻节点
|
|
||||||
const neighbors = this.getNeighbors(currentNode);
|
|
||||||
for (const neighbor of neighbors) {
|
|
||||||
if (!neighbor.walkable || closedSet.indexOf(neighbor) !== -1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newGCost = currentNode.gCost + this.getDistance(currentNode, neighbor);
|
|
||||||
|
|
||||||
if (newGCost < neighbor.gCost || openSet.indexOf(neighbor) === -1) {
|
|
||||||
neighbor.gCost = newGCost;
|
|
||||||
neighbor.hCost = this.getDistance(neighbor, targetNode);
|
|
||||||
neighbor.calculateFCost();
|
|
||||||
neighbor.parent = currentNode;
|
|
||||||
|
|
||||||
if (openSet.indexOf(neighbor) === -1) {
|
|
||||||
openSet.push(neighbor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 没有找到路径
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
if (startX === targetX && startY === targetY) {
|
||||||
* 获取节点的相邻节点(4方向)
|
return [new Vec2(startX, startY)];
|
||||||
*/
|
}
|
||||||
private getNeighbors(node: PathNode): PathNode[] {
|
|
||||||
const neighbors: PathNode[] = [];
|
|
||||||
|
|
||||||
// 四个方向:上、下、左、右
|
// 重置所有节点
|
||||||
const directions = [
|
this.resetNodes();
|
||||||
{ x: 0, y: 1 }, // 上
|
|
||||||
{ x: 0, y: -1 }, // 下
|
|
||||||
{ x: -1, y: 0 }, // 左
|
|
||||||
{ x: 1, y: 0 } // 右
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const dir of directions) {
|
const startNode = this.grid[startX][startY];
|
||||||
const checkX = node.x + dir.x;
|
const targetNode = this.grid[targetX][targetY];
|
||||||
const checkY = node.y + dir.y;
|
|
||||||
|
|
||||||
if (this.isValidPosition(checkX, checkY)) {
|
const openSet: PathNode[] = [];
|
||||||
neighbors.push(this.grid[checkX][checkY]);
|
const closedSet: PathNode[] = [];
|
||||||
}
|
|
||||||
|
openSet.push(startNode);
|
||||||
|
|
||||||
|
while (openSet.length > 0) {
|
||||||
|
// 找到f值最小的节点
|
||||||
|
let currentNode = openSet[0];
|
||||||
|
for (let i = 1; i < openSet.length; i++) {
|
||||||
|
if (openSet[i].fCost < currentNode.fCost ||
|
||||||
|
(openSet[i].fCost === currentNode.fCost && openSet[i].hCost < currentNode.hCost)) {
|
||||||
|
currentNode = openSet[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从开放列表移除当前节点,添加到关闭列表
|
||||||
|
openSet.splice(openSet.indexOf(currentNode), 1);
|
||||||
|
closedSet.push(currentNode);
|
||||||
|
|
||||||
|
// 如果到达目标节点,重建路径
|
||||||
|
if (currentNode === targetNode) {
|
||||||
|
return this.retracePath(startNode, targetNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查相邻节点
|
||||||
|
const neighbors = this.getNeighbors(currentNode);
|
||||||
|
for (const neighbor of neighbors) {
|
||||||
|
if (!neighbor.walkable || closedSet.indexOf(neighbor) !== -1) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return neighbors;
|
const newGCost = currentNode.gCost + neighbor.moveCost;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (newGCost < neighbor.gCost || openSet.indexOf(neighbor) === -1) {
|
||||||
* 获取两个节点之间的距离(曼哈顿距离)
|
neighbor.gCost = newGCost;
|
||||||
*/
|
neighbor.hCost = this.getDistance(neighbor, targetNode);
|
||||||
private getDistance(nodeA: PathNode, nodeB: PathNode): number {
|
neighbor.calculateFCost();
|
||||||
const distX = Math.abs(nodeA.x - nodeB.x);
|
neighbor.parent = currentNode;
|
||||||
const distY = Math.abs(nodeA.y - nodeB.y);
|
|
||||||
return distX + distY;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (openSet.indexOf(neighbor) === -1) {
|
||||||
* 重建路径
|
openSet.push(neighbor);
|
||||||
*/
|
}
|
||||||
private retracePath(startNode: PathNode, endNode: PathNode): Vec2[] {
|
|
||||||
const path: Vec2[] = [];
|
|
||||||
let currentNode = endNode;
|
|
||||||
|
|
||||||
while (currentNode !== startNode) {
|
|
||||||
path.push(new Vec2(currentNode.x, currentNode.y));
|
|
||||||
currentNode = currentNode.parent!;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
path.push(new Vec2(startNode.x, startNode.y));
|
|
||||||
path.reverse();
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 没有找到路径
|
||||||
* 重置所有节点的寻路参数
|
return [];
|
||||||
*/
|
}
|
||||||
private resetNodes() {
|
|
||||||
for (let x = 0; x < this.gridWidth; x++) {
|
/**
|
||||||
for (let y = 0; y < this.gridHeight; y++) {
|
* 获取节点的相邻节点(8方向)
|
||||||
const node = this.grid[x][y];
|
*/
|
||||||
node.gCost = 0;
|
private getNeighbors(node: PathNode): PathNode[] {
|
||||||
node.hCost = 0;
|
const neighbors: PathNode[] = [];
|
||||||
node.fCost = 0;
|
|
||||||
node.parent = null;
|
// 八个方向:上、下、左、右、左上、右上、左下、右下
|
||||||
}
|
const directions = [
|
||||||
|
{ x: 0, y: 1, cost: 1.0 }, // 上
|
||||||
|
{ x: 0, y: -1, cost: 1.0 }, // 下
|
||||||
|
{ x: -1, y: 0, cost: 1.0 }, // 左
|
||||||
|
{ x: 1, y: 0, cost: 1.0 }, // 右
|
||||||
|
{ x: -1, y: 1, cost: 1.414 }, // 左上
|
||||||
|
{ x: 1, y: 1, cost: 1.414 }, // 右上
|
||||||
|
{ x: -1, y: -1, cost: 1.414 },// 左下
|
||||||
|
{ x: 1, y: -1, cost: 1.414 } // 右下
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const dir of directions) {
|
||||||
|
const checkX = node.x + dir.x;
|
||||||
|
const checkY = node.y + dir.y;
|
||||||
|
|
||||||
|
if (this.isValidPosition(checkX, checkY)) {
|
||||||
|
const neighbor = this.grid[checkX][checkY];
|
||||||
|
// 为对角线移动添加额外检查,防止穿过墙角
|
||||||
|
if (dir.cost > 1.0) {
|
||||||
|
// 检查对角线移动时,相邻的两个直角方向是否可行走
|
||||||
|
const xCheck = this.grid[node.x + dir.x][node.y];
|
||||||
|
const yCheck = this.grid[node.x][node.y + dir.y];
|
||||||
|
if (!xCheck.walkable || !yCheck.walkable) {
|
||||||
|
continue; // 如果相邻的直角方向不可行走,则不能进行对角线移动
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
neighbor.moveCost = dir.cost;
|
||||||
|
neighbors.push(neighbor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return neighbors;
|
||||||
* 检查位置是否有效
|
}
|
||||||
*/
|
|
||||||
private isValidPosition(x: number, y: number): boolean {
|
/**
|
||||||
return x >= 0 && x < this.gridWidth && y >= 0 && y < this.gridHeight;
|
* 获取两个节点之间的距离(欧几里得距离,支持对角线移动)
|
||||||
|
*/
|
||||||
|
private getDistance(nodeA: PathNode, nodeB: PathNode): number {
|
||||||
|
const distX = Math.abs(nodeA.x - nodeB.x);
|
||||||
|
const distY = Math.abs(nodeA.y - nodeB.y);
|
||||||
|
|
||||||
|
// 使用欧几里得距离,更适合对角线移动
|
||||||
|
// 对角线距离约为1.414,直线距离为1
|
||||||
|
if (distX === distY) {
|
||||||
|
return distX * 1.414; // 对角线移动
|
||||||
|
} else {
|
||||||
|
return distX + distY; // 曼哈顿距离
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重建路径
|
||||||
|
*/
|
||||||
|
private retracePath(startNode: PathNode, endNode: PathNode): Vec2[] {
|
||||||
|
const path: Vec2[] = [];
|
||||||
|
let currentNode = endNode;
|
||||||
|
|
||||||
|
while (currentNode !== startNode) {
|
||||||
|
path.push(new Vec2(currentNode.x, currentNode.y));
|
||||||
|
currentNode = currentNode.parent!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
path.push(new Vec2(startNode.x, startNode.y));
|
||||||
* 设置节点的可行走状态
|
path.reverse();
|
||||||
*/
|
|
||||||
setWalkable(x: number, y: number, walkable: boolean) {
|
|
||||||
if (this.isValidPosition(x, y)) {
|
|
||||||
this.grid[x][y].walkable = walkable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return path;
|
||||||
* 获取节点是否可行走
|
}
|
||||||
*/
|
|
||||||
isWalkable(x: number, y: number): boolean {
|
/**
|
||||||
if (!this.isValidPosition(x, y)) {
|
* 重置所有节点的寻路参数
|
||||||
return false;
|
*/
|
||||||
}
|
private resetNodes() {
|
||||||
return this.grid[x][y].walkable;
|
for (let x = 0; x < this.gridWidth; x++) {
|
||||||
|
for (let y = 0; y < this.gridHeight; y++) {
|
||||||
|
const node = this.grid[x][y];
|
||||||
|
node.gCost = 0;
|
||||||
|
node.hCost = 0;
|
||||||
|
node.fCost = 0;
|
||||||
|
node.parent = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查位置是否有效
|
||||||
|
*/
|
||||||
|
private isValidPosition(x: number, y: number): boolean {
|
||||||
|
return x >= 0 && x < this.gridWidth && y >= 0 && y < this.gridHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置节点的可行走状态
|
||||||
|
*/
|
||||||
|
setWalkable(x: number, y: number, walkable: boolean) {
|
||||||
|
if (this.isValidPosition(x, y)) {
|
||||||
|
this.grid[x][y].walkable = walkable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取节点是否可行走
|
||||||
|
*/
|
||||||
|
isWalkable(x: number, y: number): boolean {
|
||||||
|
if (!this.isValidPosition(x, y)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.grid[x][y].walkable;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -41,12 +41,6 @@ export class PlayerController extends Component {
|
|||||||
@property
|
@property
|
||||||
mapHeight: number = 2560; // 地图高度
|
mapHeight: number = 2560; // 地图高度
|
||||||
|
|
||||||
@property({ tooltip: '玩家与怪物进入战斗时的理想距离' })
|
|
||||||
attackPreferredDistance: number = 10;
|
|
||||||
|
|
||||||
@property({ tooltip: '允许的距离误差范围,超出后会进行位置调整' })
|
|
||||||
attackDistanceTolerance: number = 10;
|
|
||||||
|
|
||||||
private isMoving: boolean = false;
|
private isMoving: boolean = false;
|
||||||
private isAttacking: boolean = false;
|
private isAttacking: boolean = false;
|
||||||
private currentPath: Vec3[] = [];
|
private currentPath: Vec3[] = [];
|
||||||
@@ -59,10 +53,11 @@ export class PlayerController extends Component {
|
|||||||
private isWin: boolean = false; // 游戏是否胜利(到达终点)
|
private isWin: boolean = false; // 游戏是否胜利(到达终点)
|
||||||
private currentDirection: number = 5; // 当前玩家朝向:3表示左/上,5表示右/下,默认为5
|
private currentDirection: number = 5; // 当前玩家朝向:3表示左/上,5表示右/下,默认为5
|
||||||
|
|
||||||
private hasWinTimes = 0
|
// 平滑移动相关变量
|
||||||
|
private moveTween: any = null; // 当前移动的tween对象
|
||||||
|
private lastPosition: Vec3 = new Vec3(); // 上一帧位置
|
||||||
|
|
||||||
private readonly _tempVec3A: Vec3 = new Vec3();
|
private hasWinTimes = 0
|
||||||
private readonly _tempVec3B: Vec3 = new Vec3();
|
|
||||||
|
|
||||||
// 道具列表
|
// 道具列表
|
||||||
private props: Node[] = [];
|
private props: Node[] = [];
|
||||||
@@ -225,7 +220,7 @@ export class PlayerController extends Component {
|
|||||||
if (!this.player || !this.pathfinder) return;
|
if (!this.player || !this.pathfinder) return;
|
||||||
|
|
||||||
// 停止当前移动
|
// 停止当前移动
|
||||||
this.stopMovement();
|
// this.stopMovement();
|
||||||
|
|
||||||
// 限制目标位置在地图边界内
|
// 限制目标位置在地图边界内
|
||||||
const clampedPos = this.clampPlayerPosition(worldPos);
|
const clampedPos = this.clampPlayerPosition(worldPos);
|
||||||
@@ -262,7 +257,8 @@ export class PlayerController extends Component {
|
|||||||
// 切换到对应的动画
|
// 切换到对应的动画
|
||||||
this.switchAnimation(animationName);
|
this.switchAnimation(animationName);
|
||||||
|
|
||||||
this.moveToNextWaypoint();
|
// 使用平滑路径移动
|
||||||
|
this.startSmoothPathMovement();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 限制玩家位置在地图边界内
|
// 限制玩家位置在地图边界内
|
||||||
@@ -374,12 +370,15 @@ export class PlayerController extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 停止当前的移动tween
|
||||||
|
if (this.moveTween) {
|
||||||
|
this.moveTween.stop();
|
||||||
|
this.moveTween = null;
|
||||||
|
}
|
||||||
|
|
||||||
const targetPos = this.currentPath[this.currentPathIndex];
|
const targetPos = this.currentPath[this.currentPathIndex];
|
||||||
const currentPos = this.player.position;
|
const currentPos = this.player.position;
|
||||||
|
|
||||||
|
|
||||||
// 计算移动距离和时间
|
// 计算移动距离和时间
|
||||||
const distance = Vec3.distance(currentPos, targetPos);
|
const distance = Vec3.distance(currentPos, targetPos);
|
||||||
const moveTime = distance / this.moveSpeed;
|
const moveTime = distance / this.moveSpeed;
|
||||||
@@ -388,10 +387,16 @@ export class PlayerController extends Component {
|
|||||||
|
|
||||||
// 记录目标位置用于方向判断
|
// 记录目标位置用于方向判断
|
||||||
this.lastTargetPosition.set(targetPos);
|
this.lastTargetPosition.set(targetPos);
|
||||||
|
this.lastPosition.set(currentPos);
|
||||||
|
|
||||||
// 使用缓动移动到目标位置
|
// 使用缓动移动到目标位置
|
||||||
tween(this.player)
|
this.moveTween = tween(this.player)
|
||||||
.to(moveTime, { position: targetPos }, {
|
.to(moveTime, { position: targetPos }, {
|
||||||
|
easing: 'linear', // 使用线性插值,保持匀速移动
|
||||||
|
onUpdate: (target: Node) => {
|
||||||
|
// 在移动过程中更新动画方向
|
||||||
|
this.updateMovementDirection(target.position);
|
||||||
|
},
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
this.currentPathIndex++;
|
this.currentPathIndex++;
|
||||||
this.moveToNextWaypoint();
|
this.moveToNextWaypoint();
|
||||||
@@ -400,10 +405,153 @@ export class PlayerController extends Component {
|
|||||||
.start();
|
.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始平滑路径移动
|
||||||
|
*/
|
||||||
|
private startSmoothPathMovement() {
|
||||||
|
if (!this.player || this.currentPath.length === 0) {
|
||||||
|
this.isMoving = false;
|
||||||
|
this.switchAnimation('stand');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止当前的移动tween
|
||||||
|
if (this.moveTween) {
|
||||||
|
this.moveTween.stop();
|
||||||
|
this.moveTween = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总路径长度
|
||||||
|
let totalDistance = 0;
|
||||||
|
for (let i = 0; i < this.currentPath.length - 1; i++) {
|
||||||
|
totalDistance += Vec3.distance(this.currentPath[i], this.currentPath[i + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总移动时间
|
||||||
|
const totalTime = totalDistance / this.moveSpeed;
|
||||||
|
|
||||||
|
console.log(`开始平滑路径移动,总距离: ${totalDistance.toFixed(2)}, 总时间: ${totalTime.toFixed(2)}秒`);
|
||||||
|
|
||||||
|
// 创建连续的路径移动
|
||||||
|
this.moveTween = tween(this.player)
|
||||||
|
.to(totalTime, { position: this.currentPath[this.currentPath.length - 1] }, {
|
||||||
|
easing: 'linear',
|
||||||
|
onUpdate: (target: Node, ratio: number) => {
|
||||||
|
// 根据进度计算当前位置
|
||||||
|
const currentPos = this.getPositionOnPath(ratio);
|
||||||
|
if (currentPos) {
|
||||||
|
target.position = currentPos;
|
||||||
|
this.updateMovementDirection(currentPos);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
this.isMoving = false;
|
||||||
|
this.switchAnimation('stand');
|
||||||
|
console.log('平滑路径移动完成');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据路径进度获取当前位置
|
||||||
|
*/
|
||||||
|
private getPositionOnPath(ratio: number): Vec3 | null {
|
||||||
|
if (!this.player || this.currentPath.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总路径长度
|
||||||
|
const segmentLengths: number[] = [];
|
||||||
|
let totalLength = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.currentPath.length - 1; i++) {
|
||||||
|
const length = Vec3.distance(this.currentPath[i], this.currentPath[i + 1]);
|
||||||
|
segmentLengths.push(length);
|
||||||
|
totalLength += length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算目标距离
|
||||||
|
const targetDistance = totalLength * ratio;
|
||||||
|
|
||||||
|
// 找到对应的路径段
|
||||||
|
let currentDistance = 0;
|
||||||
|
for (let i = 0; i < segmentLengths.length; i++) {
|
||||||
|
if (currentDistance + segmentLengths[i] >= targetDistance) {
|
||||||
|
// 在当前段内
|
||||||
|
const segmentRatio = (targetDistance - currentDistance) / segmentLengths[i];
|
||||||
|
const startPos = this.currentPath[i];
|
||||||
|
const endPos = this.currentPath[i + 1];
|
||||||
|
|
||||||
|
// 线性插值计算当前位置
|
||||||
|
return new Vec3(
|
||||||
|
startPos.x + (endPos.x - startPos.x) * segmentRatio,
|
||||||
|
startPos.y + (endPos.y - startPos.y) * segmentRatio,
|
||||||
|
startPos.z + (endPos.z - startPos.z) * segmentRatio
|
||||||
|
);
|
||||||
|
}
|
||||||
|
currentDistance += segmentLengths[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果超出范围,返回终点
|
||||||
|
return this.currentPath[this.currentPath.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在移动过程中更新动画方向
|
||||||
|
*/
|
||||||
|
private updateMovementDirection(currentPos: Vec3) {
|
||||||
|
if (!this.player || this.currentPath.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算移动方向(基于下一路径点)
|
||||||
|
let targetPos: Vec3;
|
||||||
|
if (this.currentPathIndex < this.currentPath.length - 1) {
|
||||||
|
targetPos = this.currentPath[this.currentPathIndex + 1];
|
||||||
|
} else {
|
||||||
|
targetPos = this.currentPath[this.currentPath.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaX = targetPos.x - currentPos.x;
|
||||||
|
const deltaY = targetPos.y - currentPos.y;
|
||||||
|
|
||||||
|
// 如果移动距离很小,不更新动画
|
||||||
|
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||||
|
if (distance < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算主要移动方向
|
||||||
|
const absX = Math.abs(deltaX);
|
||||||
|
const absY = Math.abs(deltaY);
|
||||||
|
|
||||||
|
// 根据移动方向选择动画
|
||||||
|
let animationName = 'walk';
|
||||||
|
if (absX > absY) {
|
||||||
|
// 水平移动为主
|
||||||
|
this.currentDirection = deltaX < 0 ? 3 : 5;
|
||||||
|
animationName = deltaX < 0 ? 'walk3' : 'walk5';
|
||||||
|
} else {
|
||||||
|
// 垂直移动为主
|
||||||
|
this.currentDirection = deltaY < 0 ? 3 : 5;
|
||||||
|
animationName = deltaY < 0 ? 'walk3' : 'walk5';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换到对应的动画
|
||||||
|
this.switchAnimation(animationName);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停止当前移动
|
* 停止当前移动
|
||||||
*/
|
*/
|
||||||
private stopMovement() {
|
private stopMovement() {
|
||||||
|
// 停止当前的移动tween
|
||||||
|
if (this.moveTween) {
|
||||||
|
this.moveTween.stop();
|
||||||
|
this.moveTween = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.player) {
|
if (this.player) {
|
||||||
tween(this.player).stop();
|
tween(this.player).stop();
|
||||||
}
|
}
|
||||||
@@ -447,12 +595,6 @@ export class PlayerController extends Component {
|
|||||||
|
|
||||||
this.stopMovement();
|
this.stopMovement();
|
||||||
|
|
||||||
// this.scheduleOnce(() => {
|
|
||||||
// this.adjustPositionsForAttack(otherCollider.node);
|
|
||||||
// }, 0);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 获取玩家和怪物的生命值
|
// 获取玩家和怪物的生命值
|
||||||
const playerHpLabel = this.player.getChildByName('hp');
|
const playerHpLabel = this.player.getChildByName('hp');
|
||||||
const monsterHpLabel = otherCollider.node.getChildByName('hp');
|
const monsterHpLabel = otherCollider.node.getChildByName('hp');
|
||||||
@@ -643,76 +785,6 @@ export class PlayerController extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private adjustPositionsForAttack(monsterNode: Node) {
|
|
||||||
if (!this.player || !monsterNode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const desiredDistance = Math.max(0, this.attackPreferredDistance);
|
|
||||||
const tolerance = Math.max(0, this.attackDistanceTolerance);
|
|
||||||
|
|
||||||
if (desiredDistance === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const playerWorldPos = this._tempVec3A;
|
|
||||||
const monsterWorldPos = this._tempVec3B;
|
|
||||||
this.player.getWorldPosition(playerWorldPos);
|
|
||||||
monsterNode.getWorldPosition(monsterWorldPos);
|
|
||||||
|
|
||||||
const playerTarget = playerWorldPos.clone();
|
|
||||||
const monsterTarget = monsterWorldPos.clone();
|
|
||||||
|
|
||||||
const deltaX = playerWorldPos.x - monsterWorldPos.x;
|
|
||||||
const horizontalDistance = Math.abs(deltaX);
|
|
||||||
const verticalDifference = Math.abs(playerWorldPos.y - monsterWorldPos.y);
|
|
||||||
|
|
||||||
const targetY = (playerWorldPos.y + monsterWorldPos.y) * 0.5;
|
|
||||||
let adjusted = false;
|
|
||||||
if (verticalDifference > 1e-3) {
|
|
||||||
playerTarget.y = targetY;
|
|
||||||
monsterTarget.y = targetY;
|
|
||||||
adjusted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderCorrect = playerWorldPos.x > monsterWorldPos.x;
|
|
||||||
const distanceOutOfRange = horizontalDistance < desiredDistance - tolerance || horizontalDistance > desiredDistance + tolerance;
|
|
||||||
|
|
||||||
if (!orderCorrect || distanceOutOfRange) {
|
|
||||||
const midX = (playerWorldPos.x + monsterWorldPos.x) * 0.5;
|
|
||||||
const halfDistance = desiredDistance * 0.5;
|
|
||||||
playerTarget.x = midX + halfDistance;
|
|
||||||
monsterTarget.x = midX - halfDistance;
|
|
||||||
adjusted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!adjusted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保最终目标仍然满足玩家在右侧的要求
|
|
||||||
if (playerTarget.x <= monsterTarget.x) {
|
|
||||||
const swapMidX = (playerTarget.x + monsterTarget.x) * 0.5;
|
|
||||||
const halfDistance = desiredDistance * 0.5;
|
|
||||||
playerTarget.x = swapMidX + halfDistance;
|
|
||||||
monsterTarget.x = swapMidX - halfDistance;
|
|
||||||
}
|
|
||||||
|
|
||||||
const clampedPlayer = this.clampPositionWithinMap(playerTarget);
|
|
||||||
const clampedMonster = this.clampPositionWithinMap(monsterTarget);
|
|
||||||
|
|
||||||
this.setNodeWorldPosition(this.player, clampedPlayer);
|
|
||||||
this.setNodeWorldPosition(monsterNode, clampedMonster);
|
|
||||||
|
|
||||||
this.player.getWorldPosition(playerWorldPos);
|
|
||||||
monsterNode.getWorldPosition(monsterWorldPos);
|
|
||||||
const finalHorizontalDistance = Math.abs(playerWorldPos.x - monsterWorldPos.x);
|
|
||||||
const finalVerticalOffset = Math.abs(playerWorldPos.y - monsterWorldPos.y);
|
|
||||||
console.log(`战斗位置已调整,水平距离: ${finalHorizontalDistance.toFixed(2)}, 垂直误差: ${finalVerticalOffset.toFixed(2)}`);
|
|
||||||
if (playerWorldPos.x >= monsterWorldPos.x) {
|
|
||||||
this.currentDirection = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setNodeWorldPosition(node: Node, worldPos: Vec3) {
|
private setNodeWorldPosition(node: Node, worldPos: Vec3) {
|
||||||
const parent = node.parent;
|
const parent = node.parent;
|
||||||
|
|||||||
@@ -6,245 +6,355 @@ const { ccclass, property } = _decorator;
|
|||||||
@ccclass('TiledMapPathfinder')
|
@ccclass('TiledMapPathfinder')
|
||||||
export class TiledMapPathfinder extends Component {
|
export class TiledMapPathfinder extends Component {
|
||||||
|
|
||||||
@property(TiledMap)
|
@property(TiledMap)
|
||||||
tiledMap: TiledMap | null = null;
|
tiledMap: TiledMap | null = null;
|
||||||
|
|
||||||
@property({ displayName: '可行走图层名称' })
|
@property({ displayName: '可行走图层名称' })
|
||||||
walkableLayerName: string = 'WalkableLayer';
|
walkableLayerName: string = 'WalkableLayer';
|
||||||
|
|
||||||
@property({ displayName: '瓦片尺寸' })
|
@property({ displayName: '瓦片尺寸' })
|
||||||
tileSize: number = 32;
|
tileSize: number = 32;
|
||||||
|
|
||||||
private pathfinding: AStarPathfinding | null = null;
|
private pathfinding: AStarPathfinding | null = null;
|
||||||
private mapSize: Size = new Size(0, 0);
|
private mapSize: Size = new Size(0, 0);
|
||||||
private walkableData: number[][] = [];
|
private walkableData: number[][] = [];
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
// 获取或创建寻路组件
|
// 获取或创建寻路组件
|
||||||
this.pathfinding = this.getComponent(AStarPathfinding);
|
this.pathfinding = this.getComponent(AStarPathfinding);
|
||||||
if (!this.pathfinding) {
|
if (!this.pathfinding) {
|
||||||
this.pathfinding = this.addComponent(AStarPathfinding);
|
this.pathfinding = this.addComponent(AStarPathfinding);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.tiledMap) {
|
||||||
|
this.initializePathfinding();
|
||||||
|
} else {
|
||||||
|
console.error('TiledMapPathfinder: TiledMap组件未设置');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化寻路系统
|
||||||
|
*/
|
||||||
|
private initializePathfinding() {
|
||||||
|
if (!this.tiledMap) {
|
||||||
|
console.error('TiledMap未设置');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
// 获取地图尺寸
|
||||||
if (this.tiledMap) {
|
this.mapSize = this.tiledMap.getMapSize();
|
||||||
this.initializePathfinding();
|
console.log(`地图尺寸: ${this.mapSize.width}x${this.mapSize.height}`);
|
||||||
|
|
||||||
|
// 获取可行走图层
|
||||||
|
const walkableLayer = this.tiledMap.getLayer(this.walkableLayerName);
|
||||||
|
if (!walkableLayer) {
|
||||||
|
console.error(`找不到图层: ${this.walkableLayerName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取可行走数据
|
||||||
|
this.extractWalkableData(walkableLayer);
|
||||||
|
|
||||||
|
// 初始化A*寻路算法
|
||||||
|
if (this.pathfinding) {
|
||||||
|
this.pathfinding.initializeGrid(this.mapSize.width, this.mapSize.height, this.walkableData);
|
||||||
|
console.log('寻路系统初始化完成');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从TiledLayer提取可行走数据
|
||||||
|
*/
|
||||||
|
private extractWalkableData(layer: TiledLayer) {
|
||||||
|
this.walkableData = [];
|
||||||
|
|
||||||
|
for (let y = 0; y < this.mapSize.height; y++) {
|
||||||
|
this.walkableData[y] = [];
|
||||||
|
for (let x = 0; x < this.mapSize.width; x++) {
|
||||||
|
// 获取指定位置的瓦片GID
|
||||||
|
const gid = layer.getTileGIDAt(x, y);
|
||||||
|
|
||||||
|
// GID > 0 表示有瓦片,表示可行走
|
||||||
|
// GID = 0 表示没有瓦片,表示不可行走
|
||||||
|
this.walkableData[y][x] = gid > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('可行走数据提取完成');
|
||||||
|
this.debugPrintWalkableData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调试打印可行走数据(仅打印部分数据以避免日志过长)
|
||||||
|
*/
|
||||||
|
private debugPrintWalkableData() {
|
||||||
|
console.log('可行走数据示例(前10行):');
|
||||||
|
for (let y = 0; y < Math.min(10, this.walkableData.length); y++) {
|
||||||
|
const row = this.walkableData[y].slice(0, Math.min(10, this.walkableData[y].length));
|
||||||
|
console.log(`第${y}行: [${row.join(', ')}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 寻找路径
|
||||||
|
* @param startWorldPos 起点世界坐标
|
||||||
|
* @param targetWorldPos 终点世界坐标
|
||||||
|
* @returns 路径的世界坐标数组
|
||||||
|
*/
|
||||||
|
findPath(startWorldPos: Vec3, targetWorldPos: Vec3): Vec3[] {
|
||||||
|
if (!this.pathfinding || !this.tiledMap) {
|
||||||
|
console.warn('寻路系统未初始化');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将世界坐标转换为瓦片坐标
|
||||||
|
const startTilePos = this.worldToTileCoordinate(startWorldPos);
|
||||||
|
const targetTilePos = this.worldToTileCoordinate(targetWorldPos);
|
||||||
|
|
||||||
|
console.log(`寻路: 起点瓦片坐标(${startTilePos.x}, ${startTilePos.y}) -> 终点瓦片坐标(${targetTilePos.x}, ${targetTilePos.y})`);
|
||||||
|
|
||||||
|
// 使用A*算法寻找路径
|
||||||
|
const tilePath = this.pathfinding.findPath(
|
||||||
|
startTilePos.x, startTilePos.y,
|
||||||
|
targetTilePos.x, targetTilePos.y
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tilePath.length === 0) {
|
||||||
|
console.warn('未找到路径');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将瓦片坐标路径转换为世界坐标路径
|
||||||
|
const worldPath: Vec3[] = [];
|
||||||
|
for (const tilePos of tilePath) {
|
||||||
|
const worldPos = this.tileToWorldCoordinate(tilePos);
|
||||||
|
worldPath.push(worldPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用路径平滑算法
|
||||||
|
const smoothedPath = this.smoothPath(worldPath);
|
||||||
|
|
||||||
|
console.log(`找到路径,原始${worldPath.length}个点,平滑后${smoothedPath.length}个点`);
|
||||||
|
return smoothedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 世界坐标转换为瓦片坐标
|
||||||
|
*/
|
||||||
|
private worldToTileCoordinate(worldPos: Vec3): Vec2 {
|
||||||
|
if (!this.tiledMap) {
|
||||||
|
return new Vec2(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取地图节点的位置偏移
|
||||||
|
const mapNode = this.tiledMap.node;
|
||||||
|
const mapPosition = mapNode.position;
|
||||||
|
|
||||||
|
// 计算相对于地图的坐标
|
||||||
|
const relativeX = worldPos.x - mapPosition.x;
|
||||||
|
const relativeY = worldPos.y - mapPosition.y;
|
||||||
|
|
||||||
|
// 获取地图尺寸信息
|
||||||
|
const mapSize = this.tiledMap.getMapSize();
|
||||||
|
const tileSize = this.tiledMap.getTileSize();
|
||||||
|
|
||||||
|
// 计算瓦片坐标
|
||||||
|
// 注意:Cocos Creator的Y轴向上为正,但瓦片地图的Y轴向下为正
|
||||||
|
const tileX = Math.floor((relativeX + mapSize.width * tileSize.width * 0.5) / tileSize.width);
|
||||||
|
const tileY = Math.floor((mapSize.height * tileSize.height * 0.5 - relativeY) / tileSize.height);
|
||||||
|
|
||||||
|
// 确保坐标在地图范围内
|
||||||
|
const clampedX = Math.max(0, Math.min(mapSize.width - 1, tileX));
|
||||||
|
const clampedY = Math.max(0, Math.min(mapSize.height - 1, tileY));
|
||||||
|
|
||||||
|
return new Vec2(clampedX, clampedY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 瓦片坐标转换为世界坐标
|
||||||
|
*/
|
||||||
|
private tileToWorldCoordinate(tilePos: Vec2): Vec3 {
|
||||||
|
if (!this.tiledMap) {
|
||||||
|
return new Vec3(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取地图节点的位置偏移
|
||||||
|
const mapNode = this.tiledMap.node;
|
||||||
|
const mapPosition = mapNode.position;
|
||||||
|
|
||||||
|
// 获取地图尺寸信息
|
||||||
|
const mapSize = this.tiledMap.getMapSize();
|
||||||
|
const tileSize = this.tiledMap.getTileSize();
|
||||||
|
|
||||||
|
// 计算世界坐标(瓦片中心点)
|
||||||
|
const worldX = (tilePos.x + 0.5) * tileSize.width - mapSize.width * tileSize.width * 0.5 + mapPosition.x;
|
||||||
|
const worldY = mapSize.height * tileSize.height * 0.5 - (tilePos.y + 0.5) * tileSize.height + mapPosition.y;
|
||||||
|
|
||||||
|
return new Vec3(worldX, worldY, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查指定世界坐标是否可行走
|
||||||
|
*/
|
||||||
|
isWorldPositionWalkable(worldPos: Vec3): boolean {
|
||||||
|
if (!this.pathfinding) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tilePos = this.worldToTileCoordinate(worldPos);
|
||||||
|
return this.pathfinding.isWalkable(tilePos.x, tilePos.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近的可行走位置
|
||||||
|
*/
|
||||||
|
getClosestWalkablePosition(worldPos: Vec3, maxSearchRadius: number = 5): Vec3 | null {
|
||||||
|
const centerTilePos = this.worldToTileCoordinate(worldPos);
|
||||||
|
|
||||||
|
// 螺旋搜索最近的可行走位置
|
||||||
|
for (let radius = 0; radius <= maxSearchRadius; radius++) {
|
||||||
|
for (let x = -radius; x <= radius; x++) {
|
||||||
|
for (let y = -radius; y <= radius; y++) {
|
||||||
|
// 只检查当前半径边界上的点
|
||||||
|
if (Math.abs(x) !== radius && Math.abs(y) !== radius && radius > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkX = centerTilePos.x + x;
|
||||||
|
const checkY = centerTilePos.y + y;
|
||||||
|
|
||||||
|
if (this.pathfinding && this.pathfinding.isWalkable(checkX, checkY)) {
|
||||||
|
return this.tileToWorldCoordinate(new Vec2(checkX, checkY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取地图信息
|
||||||
|
*/
|
||||||
|
getMapInfo() {
|
||||||
|
if (!this.tiledMap) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
mapSize: this.tiledMap.getMapSize(),
|
||||||
|
tileSize: this.tiledMap.getTileSize(),
|
||||||
|
orientation: this.tiledMap.getMapOrientation()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径平滑算法,减少不必要的转折点
|
||||||
|
* 使用视线检查算法,移除可以直接到达的中间点
|
||||||
|
*/
|
||||||
|
private smoothPath(path: Vec3[]): Vec3[] {
|
||||||
|
if (path.length <= 2) {
|
||||||
|
return path; // 路径太短,不需要平滑
|
||||||
|
}
|
||||||
|
|
||||||
|
const smoothedPath: Vec3[] = [];
|
||||||
|
smoothedPath.push(path[0]); // 添加起点
|
||||||
|
|
||||||
|
let currentIndex = 0;
|
||||||
|
let targetIndex = 1;
|
||||||
|
|
||||||
|
while (targetIndex < path.length - 1) {
|
||||||
|
// 尝试找到从当前点可以直接到达的最远点
|
||||||
|
let farthestReachable = targetIndex;
|
||||||
|
|
||||||
|
for (let i = targetIndex + 1; i < path.length; i++) {
|
||||||
|
if (this.hasLineOfSight(path[currentIndex], path[i])) {
|
||||||
|
farthestReachable = i;
|
||||||
} else {
|
} else {
|
||||||
console.error('TiledMapPathfinder: TiledMap组件未设置');
|
break; // 一旦发现不可达的点,停止搜索
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果找到了更远的可达点,跳过中间点
|
||||||
|
if (farthestReachable > targetIndex) {
|
||||||
|
currentIndex = farthestReachable;
|
||||||
|
smoothedPath.push(path[currentIndex]);
|
||||||
|
targetIndex = currentIndex + 1;
|
||||||
|
} else {
|
||||||
|
// 否则,添加当前目标点
|
||||||
|
smoothedPath.push(path[targetIndex]);
|
||||||
|
currentIndex = targetIndex;
|
||||||
|
targetIndex++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 确保终点被添加
|
||||||
* 初始化寻路系统
|
if (smoothedPath[smoothedPath.length - 1] !== path[path.length - 1]) {
|
||||||
*/
|
smoothedPath.push(path[path.length - 1]);
|
||||||
private initializePathfinding() {
|
|
||||||
if (!this.tiledMap) {
|
|
||||||
console.error('TiledMap未设置');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取地图尺寸
|
|
||||||
this.mapSize = this.tiledMap.getMapSize();
|
|
||||||
console.log(`地图尺寸: ${this.mapSize.width}x${this.mapSize.height}`);
|
|
||||||
|
|
||||||
// 获取可行走图层
|
|
||||||
const walkableLayer = this.tiledMap.getLayer(this.walkableLayerName);
|
|
||||||
if (!walkableLayer) {
|
|
||||||
console.error(`找不到图层: ${this.walkableLayerName}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取可行走数据
|
|
||||||
this.extractWalkableData(walkableLayer);
|
|
||||||
|
|
||||||
// 初始化A*寻路算法
|
|
||||||
if (this.pathfinding) {
|
|
||||||
this.pathfinding.initializeGrid(this.mapSize.width, this.mapSize.height, this.walkableData);
|
|
||||||
console.log('寻路系统初始化完成');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return smoothedPath;
|
||||||
* 从TiledLayer提取可行走数据
|
}
|
||||||
*/
|
|
||||||
private extractWalkableData(layer: TiledLayer) {
|
|
||||||
this.walkableData = [];
|
|
||||||
|
|
||||||
for (let y = 0; y < this.mapSize.height; y++) {
|
/**
|
||||||
this.walkableData[y] = [];
|
* 检查两点之间是否有直接的视线(是否可以直线移动)
|
||||||
for (let x = 0; x < this.mapSize.width; x++) {
|
*/
|
||||||
// 获取指定位置的瓦片GID
|
private hasLineOfSight(start: Vec3, end: Vec3): boolean {
|
||||||
const gid = layer.getTileGIDAt(x, y);
|
if (!this.pathfinding) {
|
||||||
|
return false;
|
||||||
// GID > 0 表示有瓦片,表示可行走
|
|
||||||
// GID = 0 表示没有瓦片,表示不可行走
|
|
||||||
this.walkableData[y][x] = gid > 0 ? 1 : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('可行走数据提取完成');
|
|
||||||
this.debugPrintWalkableData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 将世界坐标转换为瓦片坐标
|
||||||
* 调试打印可行走数据(仅打印部分数据以避免日志过长)
|
const startTile = this.worldToTileCoordinate(start);
|
||||||
*/
|
const endTile = this.worldToTileCoordinate(end);
|
||||||
private debugPrintWalkableData() {
|
|
||||||
console.log('可行走数据示例(前10行):');
|
// 使用Bresenham直线算法检查路径上的所有点
|
||||||
for (let y = 0; y < Math.min(10, this.walkableData.length); y++) {
|
const points = this.getLinePoints(startTile.x, startTile.y, endTile.x, endTile.y);
|
||||||
const row = this.walkableData[y].slice(0, Math.min(10, this.walkableData[y].length));
|
|
||||||
console.log(`第${y}行: [${row.join(', ')}]`);
|
for (const point of points) {
|
||||||
}
|
if (!this.pathfinding.isWalkable(point.x, point.y)) {
|
||||||
|
return false; // 路径上有障碍物
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return true; // 路径上没有障碍物
|
||||||
* 寻找路径
|
}
|
||||||
* @param startWorldPos 起点世界坐标
|
|
||||||
* @param targetWorldPos 终点世界坐标
|
|
||||||
* @returns 路径的世界坐标数组
|
|
||||||
*/
|
|
||||||
findPath(startWorldPos: Vec3, targetWorldPos: Vec3): Vec3[] {
|
|
||||||
if (!this.pathfinding || !this.tiledMap) {
|
|
||||||
console.warn('寻路系统未初始化');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将世界坐标转换为瓦片坐标
|
/**
|
||||||
const startTilePos = this.worldToTileCoordinate(startWorldPos);
|
* 使用Bresenham算法获取两点之间的所有点
|
||||||
const targetTilePos = this.worldToTileCoordinate(targetWorldPos);
|
*/
|
||||||
|
private getLinePoints(x0: number, y0: number, x1: number, y1: number): Vec2[] {
|
||||||
|
const points: Vec2[] = [];
|
||||||
|
const dx = Math.abs(x1 - x0);
|
||||||
|
const dy = Math.abs(y1 - y0);
|
||||||
|
const sx = x0 < x1 ? 1 : -1;
|
||||||
|
const sy = y0 < y1 ? 1 : -1;
|
||||||
|
let err = dx - dy;
|
||||||
|
|
||||||
console.log(`寻路: 起点瓦片坐标(${startTilePos.x}, ${startTilePos.y}) -> 终点瓦片坐标(${targetTilePos.x}, ${targetTilePos.y})`);
|
let x = x0;
|
||||||
|
let y = y0;
|
||||||
|
|
||||||
// 使用A*算法寻找路径
|
while (true) {
|
||||||
const tilePath = this.pathfinding.findPath(
|
points.push(new Vec2(x, y));
|
||||||
startTilePos.x, startTilePos.y,
|
|
||||||
targetTilePos.x, targetTilePos.y
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tilePath.length === 0) {
|
if (x === x1 && y === y1) {
|
||||||
console.warn('未找到路径');
|
break;
|
||||||
return [];
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 将瓦片坐标路径转换为世界坐标路径
|
const e2 = 2 * err;
|
||||||
const worldPath: Vec3[] = [];
|
if (e2 > -dy) {
|
||||||
for (const tilePos of tilePath) {
|
err -= dy;
|
||||||
const worldPos = this.tileToWorldCoordinate(tilePos);
|
x += sx;
|
||||||
worldPath.push(worldPos);
|
}
|
||||||
}
|
if (e2 < dx) {
|
||||||
|
err += dx;
|
||||||
console.log(`找到路径,包含${worldPath.length}个点`);
|
y += sy;
|
||||||
return worldPath;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return points;
|
||||||
* 世界坐标转换为瓦片坐标
|
}
|
||||||
*/
|
|
||||||
private worldToTileCoordinate(worldPos: Vec3): Vec2 {
|
|
||||||
if (!this.tiledMap) {
|
|
||||||
return new Vec2(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取地图节点的位置偏移
|
|
||||||
const mapNode = this.tiledMap.node;
|
|
||||||
const mapPosition = mapNode.position;
|
|
||||||
|
|
||||||
// 计算相对于地图的坐标
|
|
||||||
const relativeX = worldPos.x - mapPosition.x;
|
|
||||||
const relativeY = worldPos.y - mapPosition.y;
|
|
||||||
|
|
||||||
// 获取地图尺寸信息
|
|
||||||
const mapSize = this.tiledMap.getMapSize();
|
|
||||||
const tileSize = this.tiledMap.getTileSize();
|
|
||||||
|
|
||||||
// 计算瓦片坐标
|
|
||||||
// 注意:Cocos Creator的Y轴向上为正,但瓦片地图的Y轴向下为正
|
|
||||||
const tileX = Math.floor((relativeX + mapSize.width * tileSize.width * 0.5) / tileSize.width);
|
|
||||||
const tileY = Math.floor((mapSize.height * tileSize.height * 0.5 - relativeY) / tileSize.height);
|
|
||||||
|
|
||||||
// 确保坐标在地图范围内
|
|
||||||
const clampedX = Math.max(0, Math.min(mapSize.width - 1, tileX));
|
|
||||||
const clampedY = Math.max(0, Math.min(mapSize.height - 1, tileY));
|
|
||||||
|
|
||||||
return new Vec2(clampedX, clampedY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 瓦片坐标转换为世界坐标
|
|
||||||
*/
|
|
||||||
private tileToWorldCoordinate(tilePos: Vec2): Vec3 {
|
|
||||||
if (!this.tiledMap) {
|
|
||||||
return new Vec3(0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取地图节点的位置偏移
|
|
||||||
const mapNode = this.tiledMap.node;
|
|
||||||
const mapPosition = mapNode.position;
|
|
||||||
|
|
||||||
// 获取地图尺寸信息
|
|
||||||
const mapSize = this.tiledMap.getMapSize();
|
|
||||||
const tileSize = this.tiledMap.getTileSize();
|
|
||||||
|
|
||||||
// 计算世界坐标(瓦片中心点)
|
|
||||||
const worldX = (tilePos.x + 0.5) * tileSize.width - mapSize.width * tileSize.width * 0.5 + mapPosition.x;
|
|
||||||
const worldY = mapSize.height * tileSize.height * 0.5 - (tilePos.y + 0.5) * tileSize.height + mapPosition.y;
|
|
||||||
|
|
||||||
return new Vec3(worldX, worldY, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查指定世界坐标是否可行走
|
|
||||||
*/
|
|
||||||
isWorldPositionWalkable(worldPos: Vec3): boolean {
|
|
||||||
if (!this.pathfinding) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tilePos = this.worldToTileCoordinate(worldPos);
|
|
||||||
return this.pathfinding.isWalkable(tilePos.x, tilePos.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取最近的可行走位置
|
|
||||||
*/
|
|
||||||
getClosestWalkablePosition(worldPos: Vec3, maxSearchRadius: number = 5): Vec3 | null {
|
|
||||||
const centerTilePos = this.worldToTileCoordinate(worldPos);
|
|
||||||
|
|
||||||
// 螺旋搜索最近的可行走位置
|
|
||||||
for (let radius = 0; radius <= maxSearchRadius; radius++) {
|
|
||||||
for (let x = -radius; x <= radius; x++) {
|
|
||||||
for (let y = -radius; y <= radius; y++) {
|
|
||||||
// 只检查当前半径边界上的点
|
|
||||||
if (Math.abs(x) !== radius && Math.abs(y) !== radius && radius > 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkX = centerTilePos.x + x;
|
|
||||||
const checkY = centerTilePos.y + y;
|
|
||||||
|
|
||||||
if (this.pathfinding && this.pathfinding.isWalkable(checkX, checkY)) {
|
|
||||||
return this.tileToWorldCoordinate(new Vec2(checkX, checkY));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取地图信息
|
|
||||||
*/
|
|
||||||
getMapInfo() {
|
|
||||||
if (!this.tiledMap) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
mapSize: this.tiledMap.getMapSize(),
|
|
||||||
tileSize: this.tiledMap.getTileSize(),
|
|
||||||
orientation: this.tiledMap.getMapOrientation()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -5,5 +5,22 @@
|
|||||||
"width": 720,
|
"width": 720,
|
||||||
"height": 1334
|
"height": 1334
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"physics": {
|
||||||
|
"collisionGroups": [
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"name": "player"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"name": "npc"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"collisionMatrix": {
|
||||||
|
"0": 1,
|
||||||
|
"1": 4,
|
||||||
|
"2": 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user