navigate with shortest path

This commit is contained in:
wjsjwr 2024-11-24 02:10:17 +08:00
parent de1e986577
commit 7ce78f4046
6 changed files with 446 additions and 2 deletions

40
Node2d.cs Normal file
View File

@ -0,0 +1,40 @@
using Godot;
using System;
public partial class Node2d : CharacterBody2D
{
public const float Speed = 300.0f;
public const float JumpVelocity = -400.0f;
public override void _PhysicsProcess(double delta)
{
Vector2 velocity = Velocity;
// Add the gravity.
if (!IsOnFloor())
{
velocity += GetGravity() * (float)delta;
}
// Handle Jump.
if (Input.IsActionJustPressed("ui_accept") && IsOnFloor())
{
velocity.Y = JumpVelocity;
}
// Get the input direction and handle the movement/deceleration.
// As good practice, you should replace UI actions with custom gameplay actions.
Vector2 direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
if (direction != Vector2.Zero)
{
velocity.X = direction.X * Speed;
}
else
{
velocity.X = Mathf.MoveToward(Velocity.X, 0, Speed);
}
Velocity = velocity;
MoveAndSlide();
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,7 @@
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
public partial class Lab : Node2D
{
@ -9,10 +11,204 @@ public partial class Lab : Node2D
var label = GetNode<Label>("TitleBar/TitleBarBodyDate/DateLabel");
label.Text = Player.Timeline.InternalDate.ToLongDateString();
Player.Timeline.OnDayChanged += d => label.Text = d.ToLongDateString();
tileMap = GetNode<TileMapLayer>("TestMap");
UpdateMap();
}
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
}
private static readonly Vector2I tableTileCoord = new(0, 6);
private static readonly Vector2I waterTileCoord = new(6, 9);
private static readonly Vector2I doorTileCoord = new(9, 3);
private static readonly Vector2I axeTileCoord = new(10, 9);
private static readonly Vector2I blockTileCoord = new(4, 3);
public enum MapNodeType
{
Table,
Water,
Door,
Axe,
Block,
Road,
}
private const int MAP_WIDTH = 24;
private const int MAP_HEIGHT = 13;
private readonly MapNodeType[,] blocks = new MapNodeType[MAP_WIDTH, MAP_HEIGHT];
private TileMapLayer tileMap;
public void UpdateMap()
{
for (int i = 0; i < MAP_WIDTH; i++) {
for (int j = 0; j < MAP_HEIGHT; j++) {
var tile = tileMap.GetCellAtlasCoords(new Vector2I(i,j));
if (tile == tableTileCoord) {
blocks[i,j] = MapNodeType.Table;
} else if (tile == waterTileCoord) {
blocks[i,j] = MapNodeType.Water;
} else if (tile == doorTileCoord) {
blocks[i,j] = MapNodeType.Door;
} else if (tile == axeTileCoord) {
blocks[i,j] = MapNodeType.Axe;
} else if (tile == blockTileCoord) {
blocks[i,j] = MapNodeType.Block;
} else {
blocks[i,j] = MapNodeType.Road;
}
}
}
}
private static bool IsValidPosition(Vector2I pos)
{
int x = pos.X;
int y = pos.Y;
if (x < 0 || x >= MAP_WIDTH || y < 0 || y >= MAP_HEIGHT) {
return false;
}
return true;
}
private List<Vector2I> GetNeighbors(Vector2I pos) {
int x = pos.X;
int y = pos.Y;
var neighbor = new List<Vector2I>();
if (!IsValidPosition(pos)) {
return neighbor;
}
if (IsValidPosition(new Vector2I(x-1,y)) && blocks[x-1,y] == MapNodeType.Road) {
neighbor.Add(new Vector2I(x-1,y));
}
if (IsValidPosition(new Vector2I(x+1,y)) && blocks[x+1,y] == MapNodeType.Road) {
neighbor.Add(new Vector2I(x+1,y));
}
if (IsValidPosition(new Vector2I(x,y-1)) && blocks[x,y-1] == MapNodeType.Road) {
neighbor.Add(new Vector2I(x,y-1));
}
if (IsValidPosition(new Vector2I(x,y+1)) && blocks[x,y+1] == MapNodeType.Road) {
neighbor.Add(new Vector2I(x,y+1));
}
return neighbor;
}
public List<Vector2I> GetShortestPath(Vector2I start, Vector2I end)
{
for (int j = 0; j < MAP_HEIGHT; j++) {
string t = "";
for (int i = 0; i < MAP_WIDTH; i++) {
t += $"{blocks[i,j]} ";
}
GD.Print(t);
}
var path = new List<Vector2I>();
if (tileMap.GetCellSourceId(start) == -1 || tileMap.GetCellSourceId(end) == -1) {
return path;
}
if (start == end) {
return path;
}
GD.Print(start);
GD.Print(end);
static int vec2idx(Vector2I v) => v.Y * MAP_WIDTH + v.X;
static Vector2I idx2vec2(int i) => new(i % MAP_WIDTH, i / MAP_WIDTH);
HashSet<int> entriesIdx = new();
foreach (var e in GetNeighbors(end)) {
entriesIdx.Add(vec2idx(e));
}
if (entriesIdx.Count == 0) {
return path;
}
// Use Dijkstra algorithm to find the shortest path
int[] dist = new int[MAP_WIDTH * MAP_HEIGHT];
int[] prev = new int[MAP_WIDTH * MAP_HEIGHT];
bool[] visited = new bool[MAP_WIDTH * MAP_HEIGHT];
for (int i = 0; i < dist.Length; i++) {
dist[i] = int.MaxValue;
prev[i] = -1;
visited[i] = false;
}
dist[vec2idx(start)] = 0;
int answer = -1;
for (int i = 0; i < dist.Length; i++) {
int min_distance = int.MaxValue;
int u = -1;
for (int j = 0; j < dist.Length; j++) {
if (!visited[j] && dist[j] <= min_distance) {
min_distance = dist[j];
u = j;
}
}
GD.Print($"u={u}");
if (u == -1) {
return path;
}
if (entriesIdx.Contains(u)){
answer = u;
break;
}
visited[u] = true;
var neighbors = GetNeighbors(idx2vec2(u));
int alt = dist[u] + 1;
GD.Print($"Alt={alt}");
foreach (var neighbor in neighbors) {
int idx = vec2idx(neighbor);
if (visited[idx]) continue;
GD.Print($"Neighbor: {neighbor}, id={idx}, dist={dist[idx]}");
if (alt < dist[idx]) {
dist[idx] = alt;
prev[idx] = u;
GD.Print($"Prev({idx}) = {u}");
}
}
}
while (answer != vec2idx(start)) {
path.Add(idx2vec2(answer));
answer = prev[answer];
}
path.Reverse();
return path;
}
public Vector2I GetTypedBlock(MapNodeType nType, uint idx) {
Vector2I tileCoord;
switch(nType) {
case MapNodeType.Table:
tileCoord = tableTileCoord;
break;
case MapNodeType.Water:
tileCoord = waterTileCoord;
break;
case MapNodeType.Door:
tileCoord = doorTileCoord;
break;
case MapNodeType.Axe:
tileCoord = axeTileCoord;
break;
default:
return Vector2I.Zero;
}
var candidates = tileMap.GetUsedCellsById(0, tileCoord, -1);
idx %= (uint)candidates.Count;
return candidates[(int)idx];
}
public Vector2I Point2Coord(Vector2 pos) {
return tileMap.LocalToMap(tileMap.ToLocal(pos));
}
}

94
scripts/Student.cs Normal file
View File

@ -0,0 +1,94 @@
using Godot;
using System;
using System.Collections.Generic;
public partial class Student : CharacterBody2D
{
public float Speed { get; set; } = 10.0f;
public const float JumpVelocity = -400.0f;
public int NextType = -1;
private Queue<Vector2I> PathToGo = new();
public override void _PhysicsProcess(double delta)
{
if (PathToGo.Count == 0)
{
return;// No path to follow.
}
Vector2 velocity = new();
var nextPoint = PathToGo.Peek();
if ((int)GlobalPosition.X == nextPoint.X && (int)GlobalPosition.Y == nextPoint.Y) {
GD.Print(PathToGo.Dequeue());
}
nextPoint = PathToGo.Peek();
// if ((int)GlobalPosition.X == nextPoint.X) {
// // Move Y
// // velocity.Y = Math.Max(Speed, Math.Abs(nextPoint.Y - GlobalPosition.Y));
// velocity.Y = Speed;
// if (GlobalPosition.Y > nextPoint.Y) {
// velocity.Y = -velocity.Y;
// }
// } else if ((int)GlobalPosition.Y == nextPoint.Y) {
// // move X
// // velocity.X = Math.Max(Speed, Math.Abs(nextPoint.X - GlobalPosition.X));
// velocity.X = Speed;
// if (GlobalPosition.X > nextPoint.X) {
// velocity.X = -velocity.X;
// }
// }
if ((int)GlobalPosition.X == nextPoint.X) {
// Move Y
// velocity.Y = Math.Max(Speed, Math.Abs(nextPoint.Y - GlobalPosition.Y));
velocity.Y = Speed;
if (GlobalPosition.Y > nextPoint.Y) {
velocity.Y = -velocity.Y;
}
} else if ((int)GlobalPosition.Y == nextPoint.Y) {
// move X
// velocity.X = Math.Max(Speed, Math.Abs(nextPoint.X - GlobalPosition.X));
velocity.X = Speed;
if (GlobalPosition.X > nextPoint.X) {
velocity.X = -velocity.X;
}
}
GlobalPosition = GlobalPosition with {
X = GlobalPosition.X + velocity.X,
Y = GlobalPosition.Y + velocity.Y
};
// Velocity = velocity;
MoveAndSlide();
}
public override void _Ready()
{
base._Ready();
var bt = GetNode<BTPlayer>("BTPlayer");
var bb = bt.Blackboard;
// bb.BindVarToProperty("Stayed", this, "Speed");
// GD.Print(bb.GetVar("Stayed"));
// GD.Print($"Speed: {Speed}");
}
public void MoveFollowPath(List<Vector2I> path)
{
foreach (var p in path)
{
PathToGo.Enqueue(new Vector2I(p.X * 80 + 40, p.Y * 80 + 120));
}
}
public void GoTo(int nType) {
GD.Print($"Called with nType = {nType}");
var lab = GetParentOrNull<Lab>();
if (lab == null) {
return;
}
var block = lab.GetTypedBlock((Lab.MapNodeType)nType, GD.Randi());
MoveFollowPath(lab.GetShortestPath(lab.Point2Coord(GlobalPosition), block));
}
}

View File

@ -1,6 +1,7 @@
using Godot;
using System;
public partial class TestMap : TileMapLayer
{
/// <summary>
@ -36,4 +37,5 @@ public partial class TestMap : TileMapLayer
GD.Print("Mouse is over cell: ", cell);
}
}
}

108
student.tscn Normal file
View File

@ -0,0 +1,108 @@
[gd_scene load_steps=22 format=3 uid="uid://c413oatj0eqhu"]
[ext_resource type="Texture2D" uid="uid://bue73voyg0m8a" path="res://temp_res/kenney_tiny-dungeon/Tiles/tile_0099.png" id="1_aultg"]
[ext_resource type="Script" path="res://scripts/Student.cs" id="1_oesea"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_opr6h"]
size = Vector2(65, 74)
[sub_resource type="BlackboardPlan" id="BlackboardPlan_pdckj"]
var/Stayed/name = &"Stayed"
var/Stayed/type = 3
var/Stayed/value = 0.0
var/Stayed/hint = 0
var/Stayed/hint_string = ""
[sub_resource type="BBVariant" id="BBVariant_0reab"]
resource_name = "0"
saved_value = 0
type = 2
[sub_resource type="BBNode" id="BBNode_ae20x"]
resource_name = "."
saved_value = NodePath(".")
[sub_resource type="BTCallMethod" id="BTCallMethod_6osdv"]
node = SubResource("BBNode_ae20x")
method = &"GoTo"
args = Array[BBVariant]([SubResource("BBVariant_0reab")])
[sub_resource type="BBVariant" id="BBVariant_r6svu"]
resource_name = "1"
saved_value = 1
type = 2
[sub_resource type="BBNode" id="BBNode_0h6d4"]
resource_name = "."
saved_value = NodePath(".")
[sub_resource type="BTCallMethod" id="BTCallMethod_hlg51"]
node = SubResource("BBNode_0h6d4")
method = &"GoTo"
args = Array[BBVariant]([SubResource("BBVariant_r6svu")])
[sub_resource type="BBVariant" id="BBVariant_0kduy"]
resource_name = "2"
saved_value = 2
type = 2
[sub_resource type="BBNode" id="BBNode_3rnl4"]
resource_name = "."
saved_value = NodePath(".")
[sub_resource type="BTCallMethod" id="BTCallMethod_s6v6a"]
node = SubResource("BBNode_3rnl4")
method = &"GoTo"
args = Array[BBVariant]([SubResource("BBVariant_0kduy")])
[sub_resource type="BBVariant" id="BBVariant_ofgnx"]
resource_name = "3"
saved_value = 3
type = 2
[sub_resource type="BBNode" id="BBNode_sc5bo"]
resource_name = "."
saved_value = NodePath(".")
[sub_resource type="BTCallMethod" id="BTCallMethod_861fo"]
node = SubResource("BBNode_sc5bo")
method = &"GoTo"
args = Array[BBVariant]([SubResource("BBVariant_ofgnx")])
[sub_resource type="BTProbabilitySelector" id="BTProbabilitySelector_syngh"]
children = [SubResource("BTCallMethod_6osdv"), SubResource("BTCallMethod_hlg51"), SubResource("BTCallMethod_s6v6a"), SubResource("BTCallMethod_861fo")]
[sub_resource type="BTCooldown" id="BTCooldown_l6f38"]
children = [SubResource("BTProbabilitySelector_syngh")]
duration = 20.0
trigger_on_failure = true
cooldown_state_var = &"Stayed"
[sub_resource type="BTSequence" id="BTSequence_4yux8"]
children = [SubResource("BTCooldown_l6f38")]
[sub_resource type="BehaviorTree" id="BehaviorTree_rj16b"]
blackboard_plan = SubResource("BlackboardPlan_pdckj")
root_task = SubResource("BTSequence_4yux8")
[sub_resource type="BlackboardPlan" id="BlackboardPlan_gs1pu"]
var/Stayed/name = &"Stayed"
var/Stayed/type = 3
var/Stayed/value = -100.0
var/Stayed/hint = 0
var/Stayed/hint_string = ""
[node name="Student" type="CharacterBody2D"]
script = ExtResource("1_oesea")
[node name="Sprite2D" type="Sprite2D" parent="."]
scale = Vector2(5, 5)
texture = ExtResource("1_aultg")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
position = Vector2(0.5, 3)
shape = SubResource("RectangleShape2D_opr6h")
[node name="BTPlayer" type="BTPlayer" parent="."]
behavior_tree = SubResource("BehaviorTree_rj16b")
blackboard_plan = SubResource("BlackboardPlan_gs1pu")