Add bubbles and scout areas.

加入气泡和侦察区域。
This commit is contained in:
Cold-Mint 2024-07-07 23:10:25 +08:00
parent 4622d06e5c
commit adee87429e
Signed by: Cold-Mint
GPG Key ID: C5A9BF8A98E0CE99
10 changed files with 407 additions and 35 deletions

View File

@ -1,9 +1,10 @@
[gd_scene load_steps=9 format=3 uid="uid://cj65pso40syj5"]
[gd_scene load_steps=11 format=3 uid="uid://cj65pso40syj5"]
[ext_resource type="Script" path="res://scripts/character/AiCharacter.cs" id="1_ubaid"]
[ext_resource type="Texture2D" uid="uid://b1twcink38sh0" path="res://sprites/Player.png" id="2_eha68"]
[ext_resource type="Script" path="res://scripts/damage/DamageNumberNodeSpawn.cs" id="3_kiam3"]
[ext_resource type="PackedScene" uid="uid://sqqfrmikmk5v" path="res://prefab/ui/HealthBar.tscn" id="4_gt388"]
[ext_resource type="Script" path="res://scripts/bubble/BubbleMarker.cs" id="5_y2fh5"]
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_bb8wt"]
radius = 20.0
@ -24,7 +25,10 @@ animations = [{
}]
[sub_resource type="CircleShape2D" id="CircleShape2D_c61vr"]
radius = 129.027
radius = 82.2192
[sub_resource type="CircleShape2D" id="CircleShape2D_fowd5"]
radius = 233.808
[node name="DelivererOfDarkMagic" type="CharacterBody2D"]
collision_layer = 64
@ -80,3 +84,18 @@ shape = SubResource("CircleShape2D_c61vr")
[node name="NavigationAgent2D" type="NavigationAgent2D" parent="."]
debug_enabled = true
[node name="BubbleMarker" type="Marker2D" parent="."]
position = Vector2(0, -79)
script = ExtResource("5_y2fh5")
[node name="VisibleOnScreenEnabler2D" type="VisibleOnScreenEnabler2D" parent="."]
position = Vector2(0, 5.5)
scale = Vector2(2.04, 3.05)
[node name="ScoutArea2D" type="Area2D" parent="."]
collision_layer = 0
collision_mask = 68
[node name="CollisionShape2D" type="CollisionShape2D" parent="ScoutArea2D"]
shape = SubResource("CircleShape2D_fowd5")

View File

@ -9,7 +9,6 @@ grow_horizontal = 2
grow_vertical = 2
[node name="Label" type="Label" parent="."]
z_index = 1
layout_mode = 0
offset_right = 40.0
offset_bottom = 23.0

27
prefab/ui/plaint.tscn Normal file
View File

@ -0,0 +1,27 @@
[gd_scene format=3 uid="uid://dmiyu1y726uo8"]
[node name="Plaint" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Label" type="Label" parent="."]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -20.0
offset_top = -36.0
offset_right = 20.0
offset_bottom = 36.0
grow_horizontal = 2
grow_vertical = 2
theme_override_colors/font_color = Color(0.988235, 0.768627, 0.0980392, 1)
theme_override_font_sizes/font_size = 48
text = "!"
horizontal_alignment = 1

26
prefab/ui/query.tscn Normal file
View File

@ -0,0 +1,26 @@
[gd_scene format=3 uid="uid://bvpvdloxe4wdf"]
[node name="Query" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Label" type="Label" parent="."]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -20.0
offset_top = -36.0
offset_right = 20.0
offset_bottom = 36.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 48
text = "?"
horizontal_alignment = 1

View File

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using ColdMint.scripts.utils;
using Godot;
namespace ColdMint.scripts.bubble;
/// <summary>
/// <para>BubbleMarker</para>
/// <para>气泡位置标记</para>
/// </summary>
public partial class BubbleMarker : Marker2D
{
private readonly Dictionary<int, Node2D> _bubbleDictionary = [];
/// <summary>
/// <para>Add bubbles</para>
/// <para>添加气泡</para>
/// </summary>
/// <param name="id"></param>
/// <param name="node"></param>
/// <returns></returns>
public bool AddBubble(int id, Node2D node)
{
if (!_bubbleDictionary.TryAdd(id, node))
{
return false;
}
node.Hide();
NodeUtils.CallDeferredAddChild(this, node);
return true;
}
/// <summary>
/// <para>DisplayBubble</para>
/// <para>显示气泡</para>
/// </summary>
/// <remarks>
///<para>Display specific nodes above the creature as "bubbles", for example, question bubbles when an enemy finds the player.</para>
///<para>在生物头顶显示特定的节点作为“气泡”,例如:当敌人发现玩家后将显示疑问气泡。</para>
/// </remarks>
/// <param name="id"></param>
public void ShowBubble(int id)
{
if (!_bubbleDictionary.TryGetValue(id, out var value))
{
return;
}
value.Show();
}
/// <summary>
/// <para>Hidden bubble</para>
/// <para>隐藏气泡</para>
/// </summary>
public void HideBubble(int id)
{
if (!_bubbleDictionary.TryGetValue(id, out var value))
{
return;
}
value.Hide();
}
}

View File

@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using ColdMint.scripts.bubble;
using ColdMint.scripts.camp;
using ColdMint.scripts.stateMachine;
using ColdMint.scripts.utils;
using Godot;
namespace ColdMint.scripts.character;
@ -19,12 +22,28 @@ public sealed partial class AiCharacter : CharacterTemplate
private Vector2 _wallDetectionOrigin;
private Area2D? _attackArea;
/// <summary>
/// <para>Reconnaissance area</para>
/// <para>侦察区域</para>
/// </summary>
/// <remarks>
///<para>Most of the time, when the enemy enters the reconnaissance area, the character will issue a "question mark" and try to move slowly towards the event point.</para>
///<para>大多数情况下,当敌人进入侦察区域后,角色会发出“疑问(问号)”,并尝试向事件点缓慢移动。</para>
/// </remarks>
private Area2D? _scoutArea;
/// <summary>
/// <para>All enemies within striking distance</para>
/// <para>在攻击范围内的所有敌人</para>
/// </summary>
private List<CharacterTemplate>? _enemyInTheAttackRange;
/// <summary>
/// <para>Scout all enemies within range</para>
/// <para>在侦察范围内所有的敌人</para>
/// </summary>
private List<CharacterTemplate>? _enemyInTheScoutRange;
/// <summary>
/// <para>Obstacle detection ray during attack</para>
@ -37,6 +56,8 @@ public sealed partial class AiCharacter : CharacterTemplate
private RayCast2D? _attackObstacleDetection;
private VisibleOnScreenEnabler2D? _screenEnabler2D;
/// <summary>
/// <para>Navigation agent</para>
/// <para>导航代理</para>
@ -49,12 +70,69 @@ public sealed partial class AiCharacter : CharacterTemplate
public RayCast2D? AttackObstacleDetection => _attackObstacleDetection;
/// <summary>
/// <para>Exclamation bubble Id</para>
/// <para>感叹气泡Id</para>
/// </summary>
private const int plaintBubbleId = 0;
/// <summary>
/// <para>Query bubble Id</para>
/// <para>疑问气泡Id</para>
/// </summary>
private const int queryBubbleId = 1;
/// <summary>
/// <para>BubbleMarker</para>
/// <para>气泡标记</para>
/// </summary>
/// <remarks>
///<para>Subsequent production of dialogue bubbles can be put into the parent class for players to use.</para>
///<para>后续制作对话泡时可进其放到父类,供玩家使用。</para>
/// </remarks>
private BubbleMarker? _bubbleMarker;
public override void _Ready()
{
base._Ready();
_enemyInTheAttackRange = new List<CharacterTemplate>();
_enemyInTheScoutRange = new List<CharacterTemplate>();
_screenEnabler2D = GetNode<VisibleOnScreenEnabler2D>("VisibleOnScreenEnabler2D");
_screenEnabler2D.ScreenEntered += () =>
{
//When the character enters the screen.
//当角色进入屏幕。
ProcessMode = ProcessModeEnum.Disabled;
};
_screenEnabler2D.ScreenExited += () =>
{
//When the character leaves the screen.
//当角色离开屏幕。
ProcessMode = ProcessModeEnum.Inherit;
};
_bubbleMarker = GetNode<BubbleMarker>("BubbleMarker");
if (_bubbleMarker != null)
{
using var plaintScene = GD.Load<PackedScene>("res://prefab/ui/plaint.tscn");
var plaint = NodeUtils.InstantiatePackedScene<Node2D>(plaintScene);
if (plaint != null)
{
_bubbleMarker.AddBubble(plaintBubbleId, plaint);
}
using var queryScene = GD.Load<PackedScene>("res://prefab/ui/query.tscn");
var query = NodeUtils.InstantiatePackedScene<Node2D>(queryScene);
if (query != null)
{
_bubbleMarker.AddBubble(queryBubbleId, query);
}
}
_wallDetection = GetNode<RayCast2D>("WallDetection");
_attackArea = GetNode<Area2D>("AttackArea2D");
_scoutArea = GetNode<Area2D>("ScoutArea2D");
NavigationAgent2D = GetNode<NavigationAgent2D>("NavigationAgent2D");
if (ItemMarker2D != null)
{
@ -73,6 +151,14 @@ public sealed partial class AiCharacter : CharacterTemplate
_attackArea.BodyExited += ExitTheAttackArea;
}
if (_scoutArea != null)
{
_scoutArea.Monitoring = true;
_scoutArea.Monitorable = false;
_scoutArea.BodyEntered += EnterTheScoutArea;
_scoutArea.BodyExited += ExitTheScoutArea;
}
_wallDetectionOrigin = _wallDetection.TargetPosition;
StateMachine = new PatrolStateMachine();
StateMachine.Context = new StateContext
@ -87,34 +173,78 @@ public sealed partial class AiCharacter : CharacterTemplate
}
/// <summary>
/// <para>EnemyDetected</para>
/// <para>是否发现敌人</para>
/// <para>Display exclamation marks</para>
/// <para>显示感叹号</para>
/// </summary>
public void DispladyPlaint()
{
_bubbleMarker?.ShowBubble(plaintBubbleId);
}
public void HidePlaint()
{
_bubbleMarker?.HideBubble(plaintBubbleId);
}
/// <summary>
/// <para>Displady Query</para>
/// <para>显示疑问</para>
/// </summary>
public void DispladyQuery()
{
_bubbleMarker?.ShowBubble(queryBubbleId);
}
public void HiddenQuery()
{
_bubbleMarker?.HideBubble(queryBubbleId);
}
/// <summary>
/// <para>Whether the enemy has been detected in the reconnaissance area</para>
/// <para>侦察范围是否发现敌人</para>
/// </summary>
/// <returns>
///<para>Have you spotted the enemy?</para>
///<para>是否发现敌人</para>
/// </returns>
public bool EnemyDetected()
public bool ScoutEnemyDetected()
{
if (_enemyInTheAttackRange == null)
if (_enemyInTheScoutRange == null)
{
return false;
}
return _enemyInTheAttackRange.Count > 0;
return _enemyInTheScoutRange.Count > 0;
}
/// <summary>
/// <para>Get the first enemy to enter range</para>
/// <para>获取第一个进入范围的敌人</para>
/// <para>Get the first enemy in range</para>
/// <para>获取第一个进入侦察范围的敌人</para>
/// </summary>
/// <returns></returns>
public CharacterTemplate? GetFirstEnemy()
public CharacterTemplate? GetFirstEnemyInScoutArea()
{
if (_enemyInTheScoutRange == null || _enemyInTheScoutRange.Count == 0)
{
return null;
}
return _enemyInTheScoutRange[0];
}
/// <summary>
/// <para>Get the first enemy within striking range</para>
/// <para>获取第一个进入攻击范围的敌人</para>
/// </summary>
/// <returns></returns>
public CharacterTemplate? GetFirstEnemyInAttackArea()
{
if (_enemyInTheAttackRange == null || _enemyInTheAttackRange.Count == 0)
{
return null;
}
return _enemyInTheAttackRange[0];
}
@ -125,36 +255,95 @@ public sealed partial class AiCharacter : CharacterTemplate
{
var nextPathPosition = NavigationAgent2D.GetNextPathPosition();
var direction = (nextPathPosition - GlobalPosition).Normalized();
velocity = direction * Config.CellSize * Speed;
velocity = direction * Config.CellSize * Speed * ProtectedSpeedScale;
}
}
private void EnterTheAttackArea(Node node)
/// <summary>
/// <para>When the node enters the reconnaissance area</para>
/// <para>当节点进入侦察区域后</para>
/// </summary>
/// <param name="node"></param>
private void EnterTheScoutArea(Node node)
{
CanCauseHarmNode(node, (canCause, characterTemplate) =>
{
if (canCause && characterTemplate != null)
{
_enemyInTheScoutRange?.Add(characterTemplate);
}
});
}
/// <summary>
/// <para>When the node exits the reconnaissance area</para>
/// <para>当节点退出侦察区域后</para>
/// </summary>
/// <param name="node"></param>
private void ExitTheScoutArea(Node node)
{
if (node == this)
{
//The target can't be yourself.
//攻击目标不能是自己。
return;
}
if (node is CharacterTemplate characterTemplate)
{
//Determine if damage can be done between factions
//判断阵营间是否可造成伤害
var camp = CampManager.GetCamp(CampId);
var enemyCamp = CampManager.GetCamp(characterTemplate.CampId);
if (enemyCamp != null && camp != null)
{
var canCause = CampManager.CanCauseHarm(camp, enemyCamp);
if (canCause)
{
_enemyInTheAttackRange?.Add(characterTemplate);
}
}
_enemyInTheScoutRange?.Remove(characterTemplate);
}
}
/// <summary>
/// <para>When a node enters the attack zone</para>
/// <para>当节点进入攻击区域后</para>
/// </summary>
/// <param name="node"></param>
private void EnterTheAttackArea(Node node)
{
CanCauseHarmNode(node, (canCause, characterTemplate) =>
{
if (canCause && characterTemplate != null)
{
_enemyInTheAttackRange?.Add(characterTemplate);
}
});
}
/// <summary>
/// <para>CanCauseHarmNode</para>
/// <para>是否可伤害某个节点</para>
/// </summary>
/// <param name="node"></param>
/// <param name="action"></param>
private void CanCauseHarmNode(Node node, Action<bool, CharacterTemplate?> action)
{
if (node == this)
{
//The target can't be yourself.
//攻击目标不能是自己。
action.Invoke(false, null);
return;
}
if (node is not CharacterTemplate characterTemplate)
{
action.Invoke(false, null);
return;
}
//Determine if damage can be done between factions
//判断阵营间是否可造成伤害
var camp = CampManager.GetCamp(CampId);
var enemyCamp = CampManager.GetCamp(characterTemplate.CampId);
if (enemyCamp != null && camp != null)
{
action.Invoke(CampManager.CanCauseHarm(camp, enemyCamp), characterTemplate);
return;
}
action.Invoke(false, characterTemplate);
}
private void ExitTheAttackArea(Node node)
{
if (node == this)
@ -194,6 +383,12 @@ public sealed partial class AiCharacter : CharacterTemplate
_attackArea.BodyExited -= ExitTheAttackArea;
}
if (_scoutArea != null)
{
_scoutArea.BodyEntered -= EnterTheScoutArea;
_scoutArea.BodyExited -= ExitTheScoutArea;
}
if (StateMachine != null)
{
StateMachine.Stop();

View File

@ -38,6 +38,36 @@ public partial class CharacterTemplate : CharacterBody2D
/// </remarks>
protected const float Speed = 5f;
/// <summary>
/// <para>Speed multiplier</para>
/// <para>速度乘数</para>
/// </summary>
protected float ProtectedSpeedScale = 1f;
/// <summary>
/// <para>Speed multiplier</para>
/// <para>速度乘数</para>
/// <remarks>
///<para>Set to 0.5 to move at 50% of the normal speed.</para>
///<para>设置为0.5则以正常速度的50%移动。</para>
/// </remarks>
/// </summary>
public float SpeedScale
{
get => ProtectedSpeedScale;
set
{
if (value > 1)
{
ProtectedSpeedScale = 1;
}
else
{
ProtectedSpeedScale = value;
}
}
}
protected const float JumpVelocity = -240;
//物品被扔出后多长时间恢复与地面和平台的碰撞(单位:秒)

View File

@ -210,7 +210,7 @@ public partial class Player : CharacterTemplate
//Moving left and right
//左右移动
var axis = Input.GetAxis("ui_left", "ui_right");
velocity.X = axis * Speed * Config.CellSize;
velocity.X = axis * Speed * Config.CellSize * ProtectedSpeedScale;
//Use items
//使用物品

View File

@ -17,21 +17,24 @@ public class ChaseStateProcessor : StateProcessorTemplate
return;
}
//Get the first enemy to enter the attack range.
//获取第一次进入攻击范围的敌人。
var enemy = aiCharacter.GetFirstEnemy();
//Get the first enemy to enter the reconnaissance range.
//获取第一次进入侦察范围的敌人。
var enemy = aiCharacter.GetFirstEnemyInScoutArea();
if (enemy == null)
{
//No more enemies. Return to previous status.
//没有敌人了,返回上一个状态。
aiCharacter.HiddenQuery();
aiCharacter.SetTargetPosition(aiCharacter.GlobalPosition);
LogCat.Log("chase_no_enemy", label: LogCat.LogLabel.ChaseStateProcessor);
context.CurrentState = context.PreviousState;
}
else
{
//Chase the enemy.
//追击敌人
//Set the position of the enemy entering the range to the position we are going to.
//将进入范围的敌人位置设置为我们要前往的位置
aiCharacter.SetTargetPosition(enemy.GlobalPosition);
aiCharacter.DispladyQuery();
}
}

View File

@ -41,8 +41,10 @@ public class PatrolStateProcessor : StateProcessorTemplate
return;
}
if (aiCharacter.EnemyDetected())
if (aiCharacter.ScoutEnemyDetected())
{
//Seeing that the enemy had entered the reconnaissance area, we gave chase immediately.
//发现敌人进入侦察范围,我们立即追击。
context.CurrentState = State.Chase;
LogCat.Log("patrol_enemy_detected", label: LogCat.LogLabel.PatrolStateProcessor);
return;
@ -50,12 +52,16 @@ public class PatrolStateProcessor : StateProcessorTemplate
if (Points == null || Points.Length == 0)
{
//There are no patrol points.
//没有巡逻点。
LogCat.LogError("no_points", label: LogCat.LogLabel.PatrolStateProcessor);
return;
}
if (_originPosition == null)
{
//If there is no origin, then wait for the character to collide with the ground to set it as the origin.
//如果没有原点,那么等待角色与地面碰撞时将其设置为原点。
if (!aiCharacter.IsOnFloor())
{
LogCat.LogWarning("patrol_not_on_floor", LogCat.LogLabel.PatrolStateProcessor);
@ -69,8 +75,10 @@ public class PatrolStateProcessor : StateProcessorTemplate
var point = _originPosition + Points[_index];
var distance = aiCharacter.GlobalPosition.DistanceTo(point.Value);
if (distance < 10)
if (distance < 5)
{
//No need to actually come to the patrol point, we just need a distance to get close.
//无需真正的来到巡逻点,我们只需要一个距离接近了就可以了。
LogCat.LogWithFormat("patrol_arrival_point", LogCat.LogLabel.PatrolStateProcessor, point);
_index++;
if (_index >= Points.Length)