From adee87429ecd80c2ff5dc5266bd763b700c0b871 Mon Sep 17 00:00:00 2001 From: Cold-Mint Date: Sun, 7 Jul 2024 23:10:25 +0800 Subject: [PATCH] =?UTF-8?q?Add=20bubbles=20and=20scout=20areas.=20?= =?UTF-8?q?=E5=8A=A0=E5=85=A5=E6=B0=94=E6=B3=A1=E5=92=8C=E4=BE=A6=E5=AF=9F?= =?UTF-8?q?=E5=8C=BA=E5=9F=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prefab/entitys/DelivererOfDarkMagic.tscn | 23 +- prefab/ui/FloatLabel.tscn | 1 - prefab/ui/plaint.tscn | 27 ++ prefab/ui/query.tscn | 26 ++ scripts/bubble/BubbleMarker.cs | 65 +++++ scripts/character/AiCharacter.cs | 243 ++++++++++++++++-- scripts/character/CharacterTemplate.cs | 30 +++ scripts/character/Player.cs | 2 +- .../StateProcessor/ChaseStateProcessor.cs | 13 +- .../StateProcessor/PatrolStateProcessor.cs | 12 +- 10 files changed, 407 insertions(+), 35 deletions(-) create mode 100644 prefab/ui/plaint.tscn create mode 100644 prefab/ui/query.tscn create mode 100644 scripts/bubble/BubbleMarker.cs diff --git a/prefab/entitys/DelivererOfDarkMagic.tscn b/prefab/entitys/DelivererOfDarkMagic.tscn index 5d7f278..210854d 100644 --- a/prefab/entitys/DelivererOfDarkMagic.tscn +++ b/prefab/entitys/DelivererOfDarkMagic.tscn @@ -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") diff --git a/prefab/ui/FloatLabel.tscn b/prefab/ui/FloatLabel.tscn index a776c97..ce612b5 100644 --- a/prefab/ui/FloatLabel.tscn +++ b/prefab/ui/FloatLabel.tscn @@ -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 diff --git a/prefab/ui/plaint.tscn b/prefab/ui/plaint.tscn new file mode 100644 index 0000000..875cb78 --- /dev/null +++ b/prefab/ui/plaint.tscn @@ -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 diff --git a/prefab/ui/query.tscn b/prefab/ui/query.tscn new file mode 100644 index 0000000..7c6ac5b --- /dev/null +++ b/prefab/ui/query.tscn @@ -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 diff --git a/scripts/bubble/BubbleMarker.cs b/scripts/bubble/BubbleMarker.cs new file mode 100644 index 0000000..65eee77 --- /dev/null +++ b/scripts/bubble/BubbleMarker.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using ColdMint.scripts.utils; +using Godot; + +namespace ColdMint.scripts.bubble; + +/// +/// BubbleMarker +/// 气泡位置标记 +/// +public partial class BubbleMarker : Marker2D +{ + private readonly Dictionary _bubbleDictionary = []; + + /// + /// Add bubbles + /// 添加气泡 + /// + /// + /// + /// + public bool AddBubble(int id, Node2D node) + { + if (!_bubbleDictionary.TryAdd(id, node)) + { + return false; + } + + node.Hide(); + NodeUtils.CallDeferredAddChild(this, node); + return true; + } + + /// + /// DisplayBubble + /// 显示气泡 + /// + /// + ///Display specific nodes above the creature as "bubbles", for example, question bubbles when an enemy finds the player. + ///在生物头顶显示特定的节点作为“气泡”,例如:当敌人发现玩家后将显示疑问气泡。 + /// + /// + public void ShowBubble(int id) + { + if (!_bubbleDictionary.TryGetValue(id, out var value)) + { + return; + } + value.Show(); + } + + /// + /// Hidden bubble + /// 隐藏气泡 + /// + public void HideBubble(int id) + { + if (!_bubbleDictionary.TryGetValue(id, out var value)) + { + return; + } + value.Hide(); + } +} \ No newline at end of file diff --git a/scripts/character/AiCharacter.cs b/scripts/character/AiCharacter.cs index 638489b..e199b61 100644 --- a/scripts/character/AiCharacter.cs +++ b/scripts/character/AiCharacter.cs @@ -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; + /// + /// Reconnaissance area + /// 侦察区域 + /// + /// + ///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. + ///大多数情况下,当敌人进入侦察区域后,角色会发出“疑问(问号)”,并尝试向事件点缓慢移动。 + /// + private Area2D? _scoutArea; + /// /// All enemies within striking distance /// 在攻击范围内的所有敌人 /// private List? _enemyInTheAttackRange; + /// + /// Scout all enemies within range + /// 在侦察范围内所有的敌人 + /// + private List? _enemyInTheScoutRange; + /// /// Obstacle detection ray during attack @@ -37,6 +56,8 @@ public sealed partial class AiCharacter : CharacterTemplate private RayCast2D? _attackObstacleDetection; + private VisibleOnScreenEnabler2D? _screenEnabler2D; + /// /// Navigation agent /// 导航代理 @@ -49,12 +70,69 @@ public sealed partial class AiCharacter : CharacterTemplate public RayCast2D? AttackObstacleDetection => _attackObstacleDetection; + + /// + /// Exclamation bubble Id + /// 感叹气泡Id + /// + private const int plaintBubbleId = 0; + + /// + /// Query bubble Id + /// 疑问气泡Id + /// + private const int queryBubbleId = 1; + + /// + /// BubbleMarker + /// 气泡标记 + /// + /// + ///Subsequent production of dialogue bubbles can be put into the parent class for players to use. + ///后续制作对话泡时可进其放到父类,供玩家使用。 + /// + private BubbleMarker? _bubbleMarker; + public override void _Ready() { base._Ready(); + _enemyInTheAttackRange = new List(); + _enemyInTheScoutRange = new List(); + _screenEnabler2D = GetNode("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"); + if (_bubbleMarker != null) + { + using var plaintScene = GD.Load("res://prefab/ui/plaint.tscn"); + var plaint = NodeUtils.InstantiatePackedScene(plaintScene); + if (plaint != null) + { + _bubbleMarker.AddBubble(plaintBubbleId, plaint); + } + + using var queryScene = GD.Load("res://prefab/ui/query.tscn"); + var query = NodeUtils.InstantiatePackedScene(queryScene); + if (query != null) + { + _bubbleMarker.AddBubble(queryBubbleId, query); + } + } + _wallDetection = GetNode("WallDetection"); _attackArea = GetNode("AttackArea2D"); + _scoutArea = GetNode("ScoutArea2D"); NavigationAgent2D = GetNode("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 } /// - /// EnemyDetected - /// 是否发现敌人 + /// Display exclamation marks + /// 显示感叹号 + /// + public void DispladyPlaint() + { + _bubbleMarker?.ShowBubble(plaintBubbleId); + } + + public void HidePlaint() + { + _bubbleMarker?.HideBubble(plaintBubbleId); + } + + /// + /// Displady Query + /// 显示疑问 + /// + public void DispladyQuery() + { + _bubbleMarker?.ShowBubble(queryBubbleId); + } + + public void HiddenQuery() + { + _bubbleMarker?.HideBubble(queryBubbleId); + } + + /// + /// Whether the enemy has been detected in the reconnaissance area + /// 侦察范围是否发现敌人 /// /// ///Have you spotted the enemy? ///是否发现敌人 /// - public bool EnemyDetected() + public bool ScoutEnemyDetected() { - if (_enemyInTheAttackRange == null) + if (_enemyInTheScoutRange == null) { return false; } - return _enemyInTheAttackRange.Count > 0; + return _enemyInTheScoutRange.Count > 0; } /// - /// Get the first enemy to enter range - /// 获取第一个进入范围的敌人 + /// Get the first enemy in range + /// 获取第一个进入侦察范围的敌人 /// /// - public CharacterTemplate? GetFirstEnemy() + public CharacterTemplate? GetFirstEnemyInScoutArea() + { + if (_enemyInTheScoutRange == null || _enemyInTheScoutRange.Count == 0) + { + return null; + } + + return _enemyInTheScoutRange[0]; + } + + /// + /// Get the first enemy within striking range + /// 获取第一个进入攻击范围的敌人 + /// + /// + 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) + /// + /// When the node enters the reconnaissance area + /// 当节点进入侦察区域后 + /// + /// + private void EnterTheScoutArea(Node node) + { + CanCauseHarmNode(node, (canCause, characterTemplate) => + { + if (canCause && characterTemplate != null) + { + _enemyInTheScoutRange?.Add(characterTemplate); + } + }); + } + + /// + /// When the node exits the reconnaissance area + /// 当节点退出侦察区域后 + /// + /// + 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); } } + /// + /// When a node enters the attack zone + /// 当节点进入攻击区域后 + /// + /// + private void EnterTheAttackArea(Node node) + { + CanCauseHarmNode(node, (canCause, characterTemplate) => + { + if (canCause && characterTemplate != null) + { + _enemyInTheAttackRange?.Add(characterTemplate); + } + }); + } + + /// + /// CanCauseHarmNode + /// 是否可伤害某个节点 + /// + /// + /// + private void CanCauseHarmNode(Node node, Action 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(); diff --git a/scripts/character/CharacterTemplate.cs b/scripts/character/CharacterTemplate.cs index 573fff6..92e5df1 100644 --- a/scripts/character/CharacterTemplate.cs +++ b/scripts/character/CharacterTemplate.cs @@ -38,6 +38,36 @@ public partial class CharacterTemplate : CharacterBody2D /// protected const float Speed = 5f; + /// + /// Speed multiplier + /// 速度乘数 + /// + protected float ProtectedSpeedScale = 1f; + + /// + /// Speed multiplier + /// 速度乘数 + /// + ///Set to 0.5 to move at 50% of the normal speed. + ///设置为0.5则以正常速度的50%移动。 + /// + /// + public float SpeedScale + { + get => ProtectedSpeedScale; + set + { + if (value > 1) + { + ProtectedSpeedScale = 1; + } + else + { + ProtectedSpeedScale = value; + } + } + } + protected const float JumpVelocity = -240; //物品被扔出后多长时间恢复与地面和平台的碰撞(单位:秒) diff --git a/scripts/character/Player.cs b/scripts/character/Player.cs index 6e0068d..7548cc4 100644 --- a/scripts/character/Player.cs +++ b/scripts/character/Player.cs @@ -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 //使用物品 diff --git a/scripts/stateMachine/StateProcessor/ChaseStateProcessor.cs b/scripts/stateMachine/StateProcessor/ChaseStateProcessor.cs index 0874edd..96f68dc 100644 --- a/scripts/stateMachine/StateProcessor/ChaseStateProcessor.cs +++ b/scripts/stateMachine/StateProcessor/ChaseStateProcessor.cs @@ -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(); } } diff --git a/scripts/stateMachine/StateProcessor/PatrolStateProcessor.cs b/scripts/stateMachine/StateProcessor/PatrolStateProcessor.cs index 3b40f4b..3d7c9cd 100644 --- a/scripts/stateMachine/StateProcessor/PatrolStateProcessor.cs +++ b/scripts/stateMachine/StateProcessor/PatrolStateProcessor.cs @@ -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)