-
Notifications
You must be signed in to change notification settings - Fork 2
/
MyrAI_.cpp
1793 lines (1520 loc) · 108 KB
/
MyrAI_.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Fill out your copyright notice in the Description page of Project Settings.
#include "Control/MyrAI.h"
#include "../Creature/MyrPhyCreature.h" // управляемое существо
#include "../Artefact/MyrLocation.h" // локация - для нацеливания на компоненты маячки в составе целиковой локации
#include "../Artefact/MyrArtefact.h" // артефакт - тоже может быть целью
#include "../MyrraGameInstance.h" // глобальный мир игры
#include "../MyrraGameModeBase.h" // уровень - брать протагониста
#include "../AssetStructures/MyrLogicEmotionReactions.h" // эмоции по событиям
#include "Curves/CurveLinearColor.h" //для высчета скоростей по кривым
#include "AIModule/Classes/Perception/AIPerceptionComponent.h"
#include "AIModule/Classes/Perception/AISenseConfig_Hearing.h"
#include "PhysicalMaterials/PhysicalMaterial.h"
#include "Components/StaticMeshComponent.h"
#include "Kismet/GameplayStatics.h" // для вызова GetAllActorsOfClass
#include "DrawDebugHelpers.h" // для рисования
uint32 AMyrAI::RandVar = 1;
//свой лог
DEFINE_LOG_CATEGORY(LogMyrAI);
#define TRA(br, cmd, gain, cond) if(cond) { Drive.Gain = gain; Drive.DoThis = EAction::##cmd; MoveBranch=br; return; }
#define BEH1(b1) (me()->CurrentState == EBehaveState::##b1)
#define BEH2(b1, b2) (me()->CurrentState == EBehaveState::##b1 || me()->CurrentState == EBehaveState::##b2)
//==============================================================================================================
// конструктор
//==============================================================================================================
AMyrAI::AMyrAI(const FObjectInitializer & ObjInit)
{
// тик ИИ - редко, два раза в секунду, вообще это число надо динамически подстраивать
PrimaryActorTick.TickInterval = 0.5f;
PrimaryActorTick.bCanEverTick = true;
//модуль перцепции
AIPerception = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("AIPerception Component"));
SetPerceptionComponent(*AIPerception);
AIPerception->bEditableWhenInherited = true;
//настройки слуха, предварительные - они должны меняться при подключении к цели
ConfigHearing = CreateDefaultSubobject<UAISenseConfig_Hearing>(TEXT("Myrra Hearing"));
if (ConfigHearing)
{ ConfigHearing->HearingRange = 1000.f;
ConfigHearing->DetectionByAffiliation.bDetectEnemies = true;
ConfigHearing->DetectionByAffiliation.bDetectNeutrals = true;
ConfigHearing->DetectionByAffiliation.bDetectFriendlies = true;
AIPerception->ConfigureSense(*ConfigHearing);
}
if (HasAnyFlags(RF_ClassDefaultObject)) return;
//привзять обработчики событий
AIPerception->OnTargetPerceptionUpdated.AddDynamic(this, &AMyrAI::OnTargetPerceptionUpdated);
}
//==============================================================================================================
//событие восприятия - на данный момент, когда ловим звук или спец-сообщение от другого существа
//==============================================================================================================
void AMyrAI::OnTargetPerceptionUpdated (AActor * Actor, FAIStimulus Stimulus)
{
//это вообще бред, почему себя нельзя исключить
if (Actor == me()) return;
//выделить из актора конкретный компонент-источник-цель, единого рецепта нет, придётся разбирать
UPrimitiveComponent* ExactObj = (UPrimitiveComponent*)Actor->GetRootComponent();
if (auto L = Cast<AMyrLocation>(Actor))
ExactObj = (UPrimitiveComponent*)L->GetCurrentBeacon();
else if (auto M = Cast<AMyrPhyCreature>(Actor))
ExactObj = M->GetMesh();
//слух и другие мгнровенные воздействия без пространства
EHowSensed HowSensed = EHowSensed::HEARD;
float Strength = Stimulus.Strength;
if (Stimulus.Type == UAISense::GetSenseID<UAISense_Hearing>() && Stimulus.WasSuccessfullySensed())
{
//спецсигналы - расщепляем "силу" на целый идентификатор и дробную силу
if(Stimulus.Tag == TEXT("HeardSpecial"))
{ HowSensed = (EHowSensed)((int)Stimulus.Strength);
Strength = Strength - (int)Stimulus.Strength;
}
//проталкиваем стимул глубже - на уровень замечания и рассовывания по ячейкам целей
//UE_LOG(LogTemp, Error, TEXT("%s AI Notices %s result %s"), *me()->GetName(), *Actor->GetName(), *TXTENUM(EGoalAcceptResult, NotRes));
auto NotRes = Notice (ExactObj, HowSensed, Strength, nullptr);
}
}
//==============================================================================================================
//обработчик занятия объекта (при его появлении или в начале игры/уровня
//==============================================================================================================
void AMyrAI::OnPossess(APawn * InPawn)
{
Super::OnPossess(InPawn);
//переименовать, чтобы в отладке в редакторе отображалось имя, ассоциированное с подопечным существом
FString NewName = InPawn->GetName() + TEXT("-") + GetName();
Rename(*NewName);
UpdateSenses();
}
//==============================================================================================================
// начать игру / ввести экземпляр контроллера в игру
//==============================================================================================================
void AMyrAI::BeginPlay()
{
//интервал тика можно динамически менять - например при неходьбе нах каждый кадр?
PrimaryActorTick.TickInterval = 0.5f;
Super::BeginPlay();
if (me())
{ if (me()->IsUnderDaemon()) AIRuleWeight = 0.0f;
UAIPerceptionSystem::RegisterPerceptionStimuliSource(ME(), UAISense_Hearing::StaticClass(), ME());
}
}
//==============================================================================================================
// рутинные действия
//==============================================================================================================
void AMyrAI::Tick(float DeltaTime)
{
//часть медленных не нужных часто процедур самого тела, дабы не плодить тиков и делителей, вынесена сюда
ME()->RareTick(DeltaTime);
//внутря
Super::Tick(DeltaTime);
//возможно, рандом тяжёл, пусть он случается реже основных расчётов
RandVar = FMath::Rand();
if (RandVar == 0) RandVar = 1;
//аналоговые тяги пригнуться и развернуться (последняя скорее всего нах)
float FacingAmount = 0;
float StealthAmount = 0;
/////////////////////////////////////////////////////////////////////////////////////////////////
//пока неясно, как ИИ обрабатывать смерть, возможно просто остановить тик
//но на всякий случай просто тик укорачивается
if (me()->Dead())
{
PrimaryActorTick.TickInterval = 1.6;
AttemptSuicideAsPlayerGone();
return;//◘◘>
}else
//LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL
//#~~#~~# если нет никаких целей - искусственным образом притянуть за уши какие-то цели
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
if(Goals[PrimaryGoal].Object == Goals[SecondaryGoal].Object)
{
Drive.MoveDir = me()->GetAxisForth(); //самозавод по позиции тела
PrimaryActorTick.TickInterval = 0.6; //редкий такт
if(AttemptSuicideAsPlayerGone()) //если высран, убрать когда игрок далеко
return;//◘◘>
}else
//если есть первичная цель, значит хотя бы одна цель есть, и есть, что обсчитывать
if (Goals[PrimaryGoal].Object)
{
//обнулить курс и скорость перед аккумуляцией новых
CleanMotionParameters(0);
//LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL
//#◘◘#◘◘# есть и вторичная цель внимания, значит 2 цели, и нужно раскорячивать внимание между ними
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
if(Goals[SecondaryGoal].Object)
{
//контейнеры для результатов трассировк
FHitResult Trace1(ForceInit);
FHitResult Trace2(ForceInit);
//полностью пересчитать все парметры целей
MeasureGoal (Goals[PrimaryGoal], nullptr, DeltaTime, false, EHowSensed::ROUTINE, &Trace1);
MeasureGoal (Goals[SecondaryGoal], nullptr, DeltaTime, false, EHowSensed::ROUTINE, &Trace2);
//если разность весов изменилась, и первичная цель уже выглядит как не первичная
if (Goals[PrimaryGoal].Weight < Goals[SecondaryGoal].Weight)
{
//поменять местами индексы цели (не меняя целей)
PrimaryGoal = SecondaryGoal;
SecondaryGoal = 1 - PrimaryGoal;
//если атака в разгаре - мягко прервать, обратив движение
if (me()->DoesAttackAction())
ME()->AttackGoBack();
}
//при экстремальном превышении веса исключить вторичную и просчитать тягу для одной первичной
if (Goals[PrimaryGoal].Weight > 10 * Goals[SecondaryGoal].Weight)
{ Goals[PrimaryGoal].Weight = 1.0f;
Goals[SecondaryGoal].Weight = 0.0f;
//сразу задать глобальноую аналоговую тягу, потому что других целей движения нет
Drive.Gain = CalcAdditiveGain(Goals[PrimaryGoal], StealthAmount, FacingAmount);
//проложить путь к единственной цели
Goals[PrimaryGoal].RouteResult = Route(Goals[PrimaryGoal], Remember(Goals[PrimaryGoal].Object), &Trace1);
Drive.MoveDir = Goals[PrimaryGoal].MoveToDir;
}
//здоровая конкуренция между двумя целями
else
{
//нормализация весов, чтобы можно было взвешенно суммиировать воздействия каждой цели
const float Normalizer = 1.0 / (Goals[PrimaryGoal].Weight + Goals[SecondaryGoal].Weight);
Goals[PrimaryGoal].Weight *= Normalizer;
Goals[SecondaryGoal].Weight *= Normalizer;
//степень раскоряки между 2 целями, если близко к 1.0 - значит по пути, -1.0 = в разных сторонах
float MindSplit = (Goals[PrimaryGoal].LookAtDir | Goals[SecondaryGoal].LookAtDir);
//цели примерно в одном напавлении + главная значительно важнее вторичной = дискретно делить полномочия
bool Look2Follow1 = (MindSplit > 0.0f && Goals[PrimaryGoal].Weight > 0.6 * Normalizer);
//отдельно вычисляем тяги к целям
float Gain1 = CalcAdditiveGain(Goals[PrimaryGoal], StealthAmount, FacingAmount);
float Gain2 = CalcAdditiveGain(Goals[SecondaryGoal], StealthAmount, FacingAmount);
Drive.Gain = Gain1 + Gain2 * MindSplit;
//режим "смотрю на вторую цель, иду к первой", направление взгляда не должно искажеть направление атаки
if (Look2Follow1)
{
//маршрут рассчитать только для первой цели
Goals[PrimaryGoal].RouteResult = Route(Goals[PrimaryGoal], Remember(Goals[PrimaryGoal].Object), &Trace1);
Drive.MoveDir = Goals[PrimaryGoal].MoveToDir;
//смотреть на вторую цель только если нет атаки (иначе внимание концентрируется самой атакой на жертву)
if(me()->NoAttack()) Drive.ActDir = Goals[SecondaryGoal].LookAtDir;
}
//цели по разные стороны от нас и примерно равны по важности
//режим раскоряки с отвлекателем (прокладывается средний маршрут)
else
{
//отдельно просчитать удобные пути к двум целям
Goals[PrimaryGoal]. RouteResult = Route (Goals[PrimaryGoal], Remember(Goals[PrimaryGoal].Object), &Trace1);
Goals[SecondaryGoal].RouteResult = Route (Goals[SecondaryGoal], Remember(Goals[SecondaryGoal].Object), &Trace2);
//выбрать смешанный, средний путь
Drive.MoveDir = FMath::Lerp(Goals[PrimaryGoal].MoveToDir,Goals[SecondaryGoal].MoveToDir, Goals[SecondaryGoal].Weight);
//куда смотреть: то на одну, то на другую
if(ChancePeriod(Goals[PrimaryGoal].Weight))
Drive.ActDir = Goals[PrimaryGoal].LookAtDir;
else Drive.ActDir = Goals[SecondaryGoal].LookAtDir;
}
}
}
//LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL
//#◘◘#~~# первичная цель только одна
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
else
{
//полностью пересчитать все парметры цели
FHitResult Trace1(ForceInit);
MeasureGoal (Goals[PrimaryGoal], nullptr, DeltaTime, true, EHowSensed::ROUTINE, &Trace1);
Drive.Gain = CalcAdditiveGain(Goals[PrimaryGoal], StealthAmount, FacingAmount);
//проложить путь к единственной цели
Goals[PrimaryGoal].RouteResult = Route(Goals[PrimaryGoal], Remember(Goals[PrimaryGoal].Object), &Trace1);
Drive.MoveDir = Goals[PrimaryGoal].MoveToDir;
Drive.ActDir = Goals[PrimaryGoal].LookAtDir;
//сдвиг в группе смыслов движения: первая группа - к цели, вторая от цели, внутри группы разные способы
//if(Goals[PrimaryGoal].RouteResult == ERouteResult::Away_Directly)
// MoveMnemo = EMoveResult::Walk_From_Goal;
//else MoveMnemo = EMoveResult::Walk_To_Goal;
}
/////////////////////////////////////////////////////////////
//если есть хотя бы 1 цель и уже выполняется атака - мож помочь ей довыполнить
//ВНИМАНИЕ! - это только для чистого ИИ, иначе будет вмешиваться и не давать завершать
if (AIRuleWeight > 0.5 && me()->Attacking())
{
//быстро перепроверить применимость атаки
auto R = me()->GetAttackAction()->RetestForStrikeForAI (me(), Goal_1().Object, &Goal_1());
//если ранее начатая к подготовке атака еще актуальна для удара
if(ActOk(R))
{ PrimaryActorTick.TickInterval = 0.15; // быстрое обновление
Drive.ActDir = Goals[PrimaryGoal].LookAtDir; // смотреть строго на цель атаки
R = me()->AttackActionStrike(); // попытаться запустить напрямую
UE_LOG(LogMyrAI, Warning, TEXT("AI %s NewAttackStrike cause %s"), *me()->GetName(), *TXTENUM(EResult, R));
}
//если атаку нельзя продолжить по материальным причинам, а не потому что уже фаза не та
else
{ PrimaryActorTick.TickInterval = 0.25; // успокоить метаболизм
me()->AttackEnd();
UE_LOG(LogMyrAI, Warning, TEXT("AI %s AttackEnd cause %s"), *me()->GetName(), *TXTENUM(EResult,R));
}
}
}
////////////////////////////////////////////////////
//досужий перебор всей палитры действий, чтобы, если повезет и подходит, рандомно что-то начинать
//выполняется как для игрока так и для непися, просто ненужные, своевольные действия должны иметь ChancrForPlayer=0
for(int i=0; i<me()->GetGenePool()->Actions.Num(); i++)
{
uint8 VictimType = 255;
auto A = me()->GetGenePool()->Actions[i];
auto R = A -> IsActionFitting (me(), Goal_1().Object, Goal_1().LookAtDist, Goal_1().LookAlign, VictimType, true, true);
//UE_LOG(LogMyrAI, Warning, TEXT("AI %s Trying %s cause %s"), *me()->GetName(), *A->HumanReadableName.ToString(), *TXTENUM(EResult, R));
if(ActOk(R))
{
//если нашли атаку
if(A->IsAttack())
{
//направить ее на обидчика и стартануть
Drive.ActDir = Goal_1().LookAtDir;
ME()->AttackActionStart(i, VictimType);
}
//самодействие
else ME()->SelfActionStart(i);
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
//влияние общей памяти о пережитом событии на эмоцию
MyrLogicTick(EventMemory, false);
//новый способ отражения эмоционального метаболизма
EmoMemory.Tick();
//пересчитать интегральную эмоцию, чтобы следующие кадры использовать для анимации
RecalcIntegralEmotion();
//отсечение ненужной возни, если ИИ в теле игрока мало используется
if (AIRuleWeight < 0.1) return; //◘◘>
////////////////////////////////////////////////////////////////////////////////////
//небольшие корректировки аналоговой тяги перед принятием решения
//когда здоровье достигло 1/2 - тяга ослабляется только если изначально низка, сильные же рывки не затрагиваются
if(me()->Health < 0.5)
if(Drive.Gain < 0.9 - 0.3*me()->Health)
Drive.Gain *= 2 * me()->Health;
//когда усталость достигла 1/5 - просто домножается так, чтобы меньше 1/5 ослабляло тягу с 1 до 0
if(me()->Stamina < 0.2)
Drive.Gain *= 5 * me()->Stamina;
//когда боль достигла 1/2 - просто домножается так, чтобы меньше 1/2 ослабляло тягу с 1 до 0
if(me()->Pain > 0.5)
if(Drive.Gain < 0.9 - 0.3*me()->Pain)
Drive.Gain *= 2 * me()->Pain;
//для аддитивного установления признака
EMoveResult TempMoveMnemo = EMoveResult::NONE;
////////////////////////////////////////////////////////////////////////////////////
// из аналоговых намерений перейти посредством конечного автомата к дискретным
// решением относительно двигательного поведения
// пока неясно, заводить ли отдельный энум, или базировать на BehaveState, как
// это уже делается на уровне PhyCreature, то есть де-факто делать надстройку
// над BehaveState-конечным автоматом
////////////////////////////////////////////////////////////////////////////////////
//если вошли в триггер-объём с установленными векторами движения - отклонить ранее посчитанный вектор в соответствии
if(me()->ModifyMoveDirByOverlap(Drive.MoveDir, EWhoCame::Creature))
{
UE_LOG(LogMyrAI, Warning, TEXT("AI %s ModifyMoveDirByOverlap %s"), *me()->GetName(), *Drive.MoveDir.ToString());
Drive.MoveDir.Normalize();
}
//степень соосности желаемого курса и тела, насколько передом вперед
float KeepDir = FMath::Max(0.0f, Drive.MoveDir | me()->GetThorax()->GuidedMoveDir);
bool OnAir = !me()->GotLandedAny();
//определение высоты над землёй - пока неясно, нужно ли это для нелетающих
if(OnAir)
{ FHitResult Hit;
FCollisionQueryParams RV_TraceParams = FCollisionQueryParams(FName(TEXT("AI_TraceForAltitude")), false, me());
RV_TraceParams.AddIgnoredActor(me()); // чтобы не застили части нас
GetWorld()->LineTraceSingleByChannel (Hit, me()->GetHeadLocation(), me()->GetHeadLocation() + FVector::DownVector*10000, ECC_WorldStatic, RV_TraceParams);
FlyHeight = Hit.Distance;
}
//признак того, что хотим двигаться прочь от цели
bool Away = ((int)Goals[PrimaryGoal].RouteResult >= (int)ERouteResult::Away_Directly);
//отдельный случай для летающего существа
if (me()->CanFly())
{
//поднялись достаточно высоко, можно опускаться
float TerminalHeight = me()->GetBodyLength() * 10;
bool FlewHigh = (FlyHeight > TerminalHeight);
//достаточно далеко от цели
bool TooFar = (Goal_1().LookAtDist > me()->GetBodyLength() * 20);
//включение механик полета
TRA(3, Soar, Drive.Gain, (TooFar || Away) && (!FlewHigh)); // взлетать если надо улепетывать или птица достаточно далеко от цели (пока не надо пикировать в нее)
TRA(2, Fly, Drive.Gain, FlewHigh); // переходить с подъёма на нормальный полёт, когда очень высоко
TRA(1, Walk, Drive.Gain, OnAir && Drive.Gain > 1 && !Away && !TooFar); // пикировать, когда цель близко, тяга к ней высока
}
//очень высокая тяга
if(Drive.Gain > 1.0f)
{
//стартовая небольшая вариация скорости (вокруг единицы) - для большой тяги способ стабилизировать
float AltGain = 0.8f + 0.2f * Drive.Gain;
TRA ( 2, Crouch, AltGain, StealthAmount > 0.7 + 0.6 * Paranoia); // при высокой тяге паранойя сильно отвращает от скрадывания, и вообще хочется бежать а не ползти
TRA ( 4, Walk, 0.5 + 0.3*KeepDir, KeepDir < 0.5); // в бегу перейти на шаг, если нажо резко развернуться
TRA ( 5, Run, AltGain, true); // перейти на бег - если всё прочее не сработало, то важна только тяга
}
//очень низкая тяга
else if(Drive.Gain < 0.4f)
{
//стартовый базис скорости - периодичность
float AltGain = Period(0, 1, 2*Drive.Gain);
TRA ( 7, Crouch, AltGain, StealthAmount > 0.8); // очень исльная тяга скрадывания - чередовать скрадывание и застывание
TRA ( 8, Crouch, Drive.Gain, StealthAmount > 0.4 + 0.2 * Paranoia); // очень сильная тяга скрыться - чередовать стойку и скрадывание
TRA ( 9, Walk, AltGain, true); // по умолчанию прерывистый шаг, так как медленный шаг некрасив, если в полёте, то парение вниз
}
//нормальная тяга
else
{ TRA (10, Crouch, Drive.Gain, StealthAmount > 0.6+0.3*Paranoia); // скрадывание
TRA (11, Walk, Drive.Gain, true); // стандартное хождение по земле от или на
TRA ( 3, Soar, Drive.Gain, me()->CanFly() && Away); // при средней тяге взлёт с места только если от цели улепетываем
}
}
//==============================================================================================================
//нужно, когда существо исчезает в норе или на дистанции
//==============================================================================================================
void AMyrAI::OnUnPossess()
{
//по какому-то маразму разработчиков для одного ИИ контроллера это событие вызывается 2 раза
//сначала до отделения Pawn, потому после, когда эта переменная уже 0
if (!me()) return;
UE_LOG(LogMyrAI, Warning, TEXT("AI %s is being destroyed"), *me()->GetName());
//практика показывает, что удалять все ссылки в ИИ других персонажей надо незамедлительно и в лоб
//иначе ИИ растягивает обновление до времени после удаления объекта, что приводит к ошибкам
TArray<AActor*> FoundCreatures;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AMyrPhyCreature::StaticClass(), FoundCreatures);
for (auto& Actor : FoundCreatures)
{
//вот таким черезжопным способом исключить себя
if (Actor != this)
{
//для каждого потенциального свидетеля вызвать спец-функцию
//которая удаляет ссылку на нас
if(auto Myr = Cast<AMyrPhyCreature>(Actor)) Myr->HaveThisDisappeared(me()->GetMesh());
}
}
//остановить тик, потому что за каким-то хером он между утерей подопечного и самодестроем продолжает тичить
PrimaryActorTick.SetTickFunctionEnable(false);
//остановить дополнительную тик-функцию, а то после отъёма объекта управления она почему-то еще работает
//CineTickFunc.SetTickFunctionEnable(false);
Super::OnUnPossess();
}
//==============================================================================================================
//прозвучать "виртуально", чтобы перцепция других существ могла нас услышать
//==============================================================================================================
void AMyrAI::LetOthersNotice (EHowSensed Event, float Strength)
{ UAISense_Hearing::ReportNoiseEvent(me(), me()->GetHeadLocation(), (float)Event + FMath::Max(Strength*0.1f, 0.9f), me(), 0, TEXT("HeardSpecial")); }
//==============================================================================================================
//уведомить систему о прохождении звука ( вызывается из шага, из MyrAnimNotify.cpp / MakeStep)
//==============================================================================================================
void AMyrAI::NotifyNoise(FVector Location, float Strength)
{
//прозвучать "виртуально", чтобы перцепция других существ могла нас услышать
UAISense_Hearing::ReportNoiseEvent(me(), Location, Strength, me());
}
//==============================================================================================================
//вспомнить или запомнить встреченного актера - возвратить индекс во внутреннем массиве для быстрой адресации
//==============================================================================================================
FGestaltRelation* AMyrAI::Remember (UPrimitiveComponent* NewObj)
{
//получить индекс в контейнере
auto PRel = Memory.Find(NewObj);
if (PRel) return PRel;
//первый раз увидели - залить эмоцию из архетипичных для класса
auto& NeuGestalt = Memory.Add(NewObj);
NeuGestalt.NeverBefore = 1;
NeuGestalt.FromFColor ( ClassRelationToClass(NewObj->GetOwner()) );
return &NeuGestalt;
}
//==============================================================================================================
//если мы прокачались в чувствительности, изменить радиусы зрения, слуха...
//==============================================================================================================
void AMyrAI::UpdateSenses()
{
//контроллер не висит в пустоте - обновить
if(me())
{ ConfigHearing->HearingRange = me()->GetGenePool()->DistanceHearing;
AIPerception->RequestStimuliListenerUpdate();
}
}
//==============================================================================================================
//выполнить трассировку к цели чтобы явно пересчитать видимость цели или второстепенного объекта
//==============================================================================================================
bool AMyrAI::TraceForVisibility (UPrimitiveComponent *Object, FGestaltRelation* Gestalt, FVector ExactRadius, FHitResult* Hit)
{
//подготовить всякую хрень для трассировки, исключить себя и цель
FVector Start = me()->GetHeadLocation(); // трассировать из глаз
FCollisionQueryParams RV_TraceParams = FCollisionQueryParams(FName(TEXT("AI_TraceForVisibility")), false, me());
RV_TraceParams.bReturnPhysicalMaterial = true; // может, и не нужно
RV_TraceParams.AddIgnoredActor(me()); // чтобы не застили части нас
//собсьвенно, провести трассировку
GetWorld()->LineTraceSingleByChannel (*Hit, Start, Start + ExactRadius, ECC_WorldStatic, RV_TraceParams);
//собственно, обновление глобального флага видимости объекта
Gestalt->NowSeen = (Hit->Component.Get() == Object);
return Gestalt->NowSeen;
}
//==============================================================================================================
//забрать образ в текущую операционную часть ИИ - и вернуть указатель на ячейку, где вся подноготная варится
//==============================================================================================================
EGoalAcceptResult AMyrAI::Notice (UPrimitiveComponent* NewGoalObj, EHowSensed HowSensed, float Strength, FGoal** WhatNoticed)
{
EGoalAcceptResult WhatToReturn; // что возвращатать (чтобы еще пошаманить с результатом до выхода)
auto Gestalt = Remember(NewGoalObj); // образ, созданный только что или уже
int GoalToAddIndex = Gestalt->InGoal0 ? 0 : (Gestalt->InGoal1 ? 1 : 2); // номер ячейки цели, в которую добавляется этот образ
//МОДИФИКАЦИЯ, если образ ранее был запомнен как находящийся в одной из ячеек
if(GoalToAddIndex < 2)
{
//разместить новые данные (но метрики не пересчитывать для оптимизации, дождаться тика)
PraePerceive(Goals[GoalToAddIndex], HowSensed, Strength, NewGoalObj, Gestalt);
WhatToReturn = (EGoalAcceptResult)((int)EGoalAcceptResult::ModifyFirst + GoalToAddIndex); //◘◘>
PostPerceive(Goals[GoalToAddIndex], HowSensed, Strength, Gestalt, WhatToReturn);
}
//если образ не находится ни в одной из ячеек
else
{
//номер ячейки цели на основе свободности ячейки 0
GoalToAddIndex = (bool)(Goals[0].Object);
//если в полученной ячейке пусто, то разместить стимул в ней
if(!Goals[GoalToAddIndex].Object)
{
//разместить новые данные на пустой ячейке - значит, полностью пересчитать (MeasureGoal)
PraePerceive(Goals[GoalToAddIndex], HowSensed, Strength, NewGoalObj, Gestalt);
MeasureGoal (Goals[GoalToAddIndex], Gestalt, 0, true, HowSensed);
WhatToReturn = (EGoalAcceptResult)((int)EGoalAcceptResult::AdoptFirst + GoalToAddIndex); //◘◘>
PostPerceive(Goals[GoalToAddIndex], HowSensed, Strength, Gestalt, WhatToReturn);
}
//если вторая ячейка занята, значит все ячейки заняты
else
{
//создаём 3ью псевдоцель, оцениваем ее вес, но PostPerceive пока рано
FGoal CandidateGoal;
PraePerceive(CandidateGoal, HowSensed, Strength, NewGoalObj, Gestalt);
MeasureGoal(CandidateGoal, Gestalt, 0, true, HowSensed);
//если новая цель более весомая, чем вторичная (а вторичная априори бессмысленней первичной)
if (RecalcGoalWeight(Goals[SecondaryGoal]) < CandidateGoal.Weight)
{
//заменить ячейку (не забыв выгрузить старую) и пометить адрес в гешатльте
Forget(Goals[SecondaryGoal], Gestalt);
Goals[SecondaryGoal] = CandidateGoal;
if(&Goals[SecondaryGoal] == &Goals[0]) Gestalt->InGoal0 = true; else Gestalt->InGoal1 = true;
WhatToReturn = EGoalAcceptResult::ReplaceSecond; //◘◘>
//№№№№№№№№№№№№№№№№№№№№№№№№№№№№№
//остальные MyrLogicEvent вызываются в PostPerceive, но это событие видно лишь отсюда
ME()->CatchMyrLogicEvent(EMyrLogicEvent::ObjDroppedOtherForYou, 1.0f, CandidateGoal.Object);//##>
//угнездить воспринятый объект теперь уже не во временной, а в постоянной ячейке цели
PostPerceive(Goals[SecondaryGoal], HowSensed, Strength, Gestalt, WhatToReturn);
}
//отбросить цель - локальная переменная CandidateGoal погибнет напрасно, PostPerceive не вызывается
else WhatToReturn = EGoalAcceptResult::Discard; //◘◘>
}
}
return WhatToReturn;
}
//==============================================================================================================
//забыть из операционной памяти, пересохранив в вдолговременную память
//==============================================================================================================
void AMyrAI::Forget(FGoal& Goal, FGestaltRelation* Gestalt)
{
//изменяем глубинное отношение к цели по текущей эмоции настолько, насколько уверенны, что узнали цель
Gestalt->SaveEmotionToLongTermMem( Goal.EventMemory );
//убрать флаг "никогда раньше" потому что теперь гешатльт считается изменён живывм опытом
Gestalt->NeverBefore = 0;
//удалить из ячейки упоминание о гештальте
if (&Goal == &Goals[0]) Gestalt->InGoal0 = 0; else Gestalt->InGoal1 = 0;
UE_LOG(LogMyrAI, Warning, TEXT(" - AI %s forgets %s"), *me()->GetName(), *Goal.Object->GetName());
Goal = FGoal();
}
//==============================================================================================================
// внести образ в ячейку цели или поторомшить имеющуюся цель сенсорным сигналом
// вызывается только в акте перцепции, когда определена ячейка FGoal (даже если она временная и ненастоящяя)
//==============================================================================================================
void AMyrAI::PraePerceive (
FGoal& Goal, // оперативная цель, объект которой был вновь замечен (или вновь создаваемая для этого объекта)
EHowSensed HowSensed, // подробный тип стимула
float Strength, // сила стимула - изнутри ИИ системы
UPrimitiveComponent* Obj, // объект-ключ, с которым свящана эта цель внимания
FGestaltRelation* NeuGestalt) // образ в долговременной памяти с архетипами
{
//замеченный образ впервые заливается в пустую ячейку цели внимания
if(!Goal.Object)
{
//занять пустую ячейку цели
Goal.Object = Obj;
// предварительное значение слышимости (хз зачем здесь)
Goal.Audibility = FMath::Min (Strength, 0.1f);
//застолбить в гешатльте, в какую ячейку мы поместили цель
//осторожно! эта функция может быть вызвана для кандидата на цели, и тогда адрес будет левым, поэтому 2 проверки
if(&Goal == &Goals[0]) NeuGestalt->InGoal0 = true; else
if(&Goal == &Goals[1]) NeuGestalt->InGoal1 = true;
}
}
//==============================================================================================================
// среагировать на перцепцию (нового или очередного) объекта
// вызывается только в событиях перцепции новых объектов, после выполнения минимальной части Measure.Goal
// здесь же происходит обработка слуха о специальных событиях (атака, смерть другого существа)
//==============================================================================================================
void AMyrAI::PostPerceive(FGoal& Goal, EHowSensed HowSensed, float Strength, FGestaltRelation* Gestalt, EGoalAcceptResult Result)
{
//разбор служебных сообщений, посланных через систему слуха
switch (HowSensed)
{
//стимул звука - обновить уровень слышимости цели (для этого расстояния уже должны быть посчитаны)
case EHowSensed::HEARD:
RecalcGoalAudibility(Goal, Strength, Gestalt);
break;
//услышанный зверь (уже измеренный и упакованный в Goal) кого-то атаковал, может, нас
case EHowSensed::ATTACKSTRIKE:
case EHowSensed::ATTACKSTART:
{ auto Res = BewareAttack(Goal.Object, &Goal, Gestalt, HowSensed == EHowSensed::ATTACKSTRIKE);
RecalcGoalAudibility(Goal, Strength, Gestalt);
UE_LOG(LogTemp, Error, TEXT("%s BewareAttack(%s) = %s"), *me()->GetName(),
*TXTENUM(EHowSensed, HowSensed), *TXTENUM(EAttackEscapeResult, Res));
break;
}
//принят сигнал о смерти этой цели (но сама цель всё ещё валяется трупом)
//сброс уверенности в узнавании, чтобы впитать в себя архетипическое отношения к мертвым данного класса
case EHowSensed::DIED:
Goal.Sure() = 0.0f;
break;
//принятие экспрессивного самодействия, типа клича угрозы, тут пока неясно, как
case EHowSensed::EXPRESSED:
//напрямую вписать в память событие впечатленности самодействием, напрямую взяв из самодействия данные для него
if (me()->DoesSelfAction())
ME()->CatchMyrLogicEvent(EMyrLogicEvent::MePatient_AffectByExpression, 1.0, Goal.Object, &me()->GetSelfAction()->EmotionalInducing);
else UE_LOG(LogTemp, Error, TEXT("%s WTF Expressed, but no self action"), *me()->GetName());
//так как это типа слух, пометить что мы услышали и увеличить / вернуть слышимость
RecalcGoalAudibility(Goal, Strength, Gestalt);
break;
//зов неживого аттрактора: сначала как слух для накрутки веса
case EHowSensed::CALL_OF_HOME:
RecalcGoalAudibility(Goal, 3.0f, Gestalt);
break;
}
//обнуление счётчика времени без событий - ведь произошло событие
//неясно, нужно ли это теперь, когда эмоции считаются другим образом
Goal.TicksNoEvents = 0;
//первое восприятие этого существа вообще в жизни - эмоции ориентируются на узнавание класса существ
if (Gestalt->NeverBefore)
{
//№№№№№№№№№№№№№№№№№№№№№№№№№№№№№
ME()->CatchMyrLogicEvent(EMyrLogicEvent::ObjNoticedFirstTime, 1.0f, Goal.Object);
Goal.EventMemory.Emotion = me()->ClassRelationToClass(Goal.Object->GetOwner());
Gestalt->NeverBefore = false;
}
}
//==============================================================================================================
//найти/перерассчитать расстояние, направление, а из них - слышимость, видимость, уверенность, абсолютный вес
//самая тяжёлая функция, вызывается в медленном тике, не в событиях слышки (которые могут прилетать с любой частотой)
//==============================================================================================================
void AMyrAI::MeasureGoal (FGoal& Goal, //ячейка цели
FGestaltRelation* Gestalt, //внецелевой образ
float DeltaTime, //время для синхронизации с реальным временем
bool Sole, //признак того, что цель одна и вес пересчитывать не надо
EHowSensed HowSensed, //событие, приведшее к замечанию цели - для ветвления/оптимизации
FHitResult* OutVisTrace) //сборка результатов трассировки видисости - нужно дальше, для поиска пути, когда будет известно, на цель или от цели
{
//если введенная цель не соответствует адресам жестких ячеек - это режим оценки новой цели и текущий Goal - временная переменная
bool TestNewGoal = (&Goal != &Goals[0])&&(&Goal != &Goals[1]);
//долгопамятный образ
if(!Gestalt) Gestalt = Remember(Goal.Object);
/////////////////////////////////////////////////////////////////////////////////
//предварительно рассчитать направление на цель и расстояние до цели
//подготовить переменные: квадрат расстояния и цель как существо
float d2 = 1000; AMyrPhyCreature* GoalMyr = Cast<AMyrPhyCreature>(Goal.Object->GetOwner());
Goal.Relativity = EYouAndMe::Unrelated;
Goal.RouteResult = ERouteResult::Towards_Directly;
FHitResult VisHit(ForceInit);
//если цель - сущство, имеется шанс более подробно
if(GoalMyr)
{
//глядим на произвольно выбранную часть тела
//это пока предварительный вектор, ненормированный
VisHit.ImpactPoint = GoalMyr->GetVisEndPoint();
//предварительный полный радиус-вектор, чтоб прикинуть, нужны ли более сложные вычисления
FVector3f TempLookAtDir = (FVector3f)(VisHit.ImpactPoint - me()->GetHeadLocation());
//также предварительно ракурс и расстояние
Goal.LookAlign = TempLookAtDir | me()->GetLookingVector();
d2 = TempLookAtDir.SizeSquared();
//если соперник перед лицом (а не за задом) и если соперник на расстоянии меньшем трёх наших длин тел
if (Goal.LookAlign > 0 && d2 < FMath::Square(2 * me()->GetBodyLength()))
{
//значит он предельно близко, и нужно вычислять расстояния до ближайшего членика его тела
VisHit.ImpactPoint = GoalMyr->GetClosestBodyLocation(me()->GetHeadLocation(), VisHit.ImpactPoint, d2);
//на таком малом расстоянии перевычисление ближайших точек приводит к резкому дерганию головой
//чтобы его сгладить, берется предыдущий полный радиус вектор
Goal.LookAtDir = FMath::Lerp(
Goal.LookAtDir*Goal.LookAtDist,
(FVector3f)(VisHit.ImpactPoint - me()->GetHeadLocation()),
0.3f);
//перерасчёт предвариительного суждение спереди/сзади
Goal.LookAlign = Goal.LookAtDir | me()->GetLookingVector();
//на таком близком расстоянии пора бы проверить наличие непосредственного контакта
if (me()->IsTouchingThisComponent(Goal.Object))
Goal.Relativity = EYouAndMe::Touching;
}
//далеко от цели, предудщее значение не нужно, можно его переписать
else Goal.LookAtDir = TempLookAtDir;
}
//цель - произвольный объект,
else
{
//смотрим в центр координат габаритов компонента
VisHit.ImpactPoint = Goal.Object->Bounds.GetSphere().Center;
Goal.LookAtDir = (FVector3f)(VisHit.ImpactPoint - me()->GetHeadLocation());
//расположение цели по отношению к нашему полю зрения
//если цель за затылком, нет смысла вычислять точно
//ВНИМАНИЕ, это предвариетнльное ненормированное значение
Goal.LookAlign = Goal.LookAtDir | me()->GetLookingVector();
}
//расстояние по прямой до объекта - временный флаг, чтобы распознать случай заполнения
Goal.LookAtDist = -1;
/////////////////////////////////////////////////////////////////////////////////
//уточнить расположение цели трассировкой - типа зрение, видимость, что застит
//сюда сохранится увиденное препятствие
//трассировку (зрение) делать только если объект впереди нас
//и если не касается нашего тела
if(Goal.LookAlign > 0 && Goal.Relativity != EYouAndMe::Touching)
{
//если в результате трассировки объект виден (нет загораживателей)
if(TraceForVisibility(Goal.Object, Gestalt, (FVector)Goal.LookAtDir, &VisHit))
{
//взять расстояние уже посчитанное из трассировки
if(VisHit.Distance>0)
{ Goal.LookAtDist = VisHit.Distance;
Goal.LookAtDistInv = 1/VisHit.Distance;
d2 = Goal.LookAtDist * Goal.LookAtDist;//для теста
}
}
}
//если не произошло заполнения расстояния через трассировку, расчёт классический
if(Goal.LookAtDist<0)
{ d2 = Goal.LookAtDir.SizeSquared();
Goal.LookAtDistInv = FMath::InvSqrt(d2);
Goal.LookAtDist = FMath::Sqrt(d2);
}
//нормализация через умножение на обратную норму
Goal.LookAtDir *= Goal.LookAtDistInv;
//здесь мы брали скалярное произведение одного единичного и одного длинного, теперь также надо избавиться от нормы
Goal.LookAlign *= Goal.LookAtDistInv;
//отладочная информация
me()->Line(ELimbDebug::AILookDir, me()->GetHeadLocation(), (FVector)Goal.LookAtDir * Goal.LookAtDist, FColor(Goal.Visibility * 255, Gestalt->NowSeen ? 255 : 0, 255, 255), 1, 0.5);
/*#if DEBUG_LINE_AI
UE_LOG(LogMyrAI, Log, TEXT("AI %s MeasureGoal %s, dist %g %s"), *me()->GetName(), *Goal.Object->GetOwner()->GetName(), FMath::Sqrt(d2), *VisHit.Location.ToString());
if(!me()->IsUnderDaemon() && HowSensed == EHowSensed::ROUTINE)
DrawDebugLine(GetWorld(), me()->GetHeadLocation(), me()->GetHeadLocation() + Goal.LookAtDir * Goal.LookAtDist,
FColor(Goal.Visibility*255, Gestalt->NowSeen ? 255 : 0, 255, 255), false, 0.5, 100, 1);
#endif*/
/////////////////////////////////////////////////////////////////////////////////
//рассчитать уровни видимости, слышимости, уверенности, что опознали цель
//полное сложное обновление, ибо события перцепции - только когда открывается и скрывается из виду
RecalcGoalVisibility (Goal, GoalMyr, Gestalt);
//насколько мы слышим цель - здесь, вне событий звука просто постепенно гасится
Goal.Audibility = FMath::Max(0.0f,
Goal.Audibility - // предыдущее значение (далее - то, что вычитается)
DeltaTime * 0.1f * // чтобы звон в ушах слабел равномерно по времени
(2.2f - Goal.Weight - Goal.Visibility)); // чем весомее цель, тем дольше верится, что звука от нее нет
//Sure - память "я опознал тебя", для стелса, если меньше 1, использовать видимость/слышимость для уверения
if (Goal.Sure() < 0.9f)
{
//скорость опознавания, связанная с тем, первый ли раз мы вообще видим эту цель, и нашим самочувствием
//здесь пока неясно, смотреть все события или только последнее
float AdvRecogMult = (Gestalt->NeverBefore) ? 0.1 : 0.3;
if(Goal.EventMemory.Last() == EMyrLogicEvent::ObjDroppedOtherForYou) AdvRecogMult *= 2;
AdvRecogMult *= me()->Health * me()->Stamina;
//квадрат видимости, слышимости минус убытие, чтобы без стимулов (если хорошо затаиться) постепенно задаваться вопросом "а был ли мальчик?"
float NewSure = Goal.Sure() + AdvRecogMult * ( FMath::Square(Goal.Visibility) * 0.7f + FMath::Square(Goal.Audibility) * 0.3f) - 0.01f;
if (NewSure > 0.0f) NewSure = 0.0f;
//применение новой степени уверенности и отлов события полной уверенности
//вообще это неправильно, событие полного узнавание должно быть только один раз, а не после каждого склероза
//но ради этого вводить лишний флаг в гештальт - пока неясно стоит ли
if (NewSure >= 1.0f)
{
//№№№№№№№№№№№№№№№№№№№№№№№№№
ME()->CatchMyrLogicEvent(EMyrLogicEvent::ObjRecognizedFirstTime, 1.0f, Goal.Object);
Gestalt->NeverBefore = 0; //завершенный акт узнавания сбрасывает факт "первый раз встретил"
Goal.Sure() = 1.0f;
} else Goal.Sure() = NewSure;
}
//мы абсолютно уверенны, что знаем эту цель, но так сучно и со временем уверенность должна немного падать, типа склероз и вообще философия
else if (Goal.Visibility + Goal.Audibility < 0.2 && Goal.TicksNoEvents>128) Goal.Sure() -= 0.01;
//если мы тестируем новую цель, то сразу же определяем вес и уходим, не надо тут трассировки
//может, даже стоит еще раньше сокращенно это все вычислить, или ввести более быструю функцию прикидку веса
if(TestNewGoal)
{ Goal.Weight = RecalcGoalWeight(Goal);
return;//◘◘>
}
//кэширование веса на ближайший такт, чтоб не пересчитывать это нагромождение влияний
//однако, если известно, что цель одна, избегать этих вычислений и сразу ставить единицу
Goal.Weight = Sole ? 1.0f : RecalcGoalWeight(Goal);
//влияние памяти о пережитом событии на эмоцию
MyrLogicTick(Goal.EventMemory, true);
//если нужно выдать подробные результаты трассировки в пространстве (там содержатся объекты
if (OutVisTrace) *OutVisTrace = VisHit;
//счётчик скукоты, сбрасывается актами замечания, атаки
if(Goal.TicksNoEvents < 255) Goal.TicksNoEvents++;
//если скукота достигла предела, цель может быть уже не нужна
}
//==============================================================================================================
//получить (из кривых от расстояния до цели) аналоговые, биполярные прикидки для тяги (вызращается)
//также поцельно аккумулировать некоторые параметры, которые собираются для всего ИИ каждый такт
//==============================================================================================================
float AMyrAI::CalcAdditiveGain(FGoal& Goal, float& StealthAmount, float& FacingAmount)
{
//аргумент для кривых - приведенная обратная дальность [0-1] = [бесконечность-касание]
float DistFactor;
//если мы касаемся противника - установить расстояние в единицу, чтобы не люфтило, ибо особый случай
if (Goal.Relativity == EYouAndMe::Touching) DistFactor = 1.0;
else
{
//проеобразовать цель в существо, если это возможно
auto MyrGoal = Cast<AMyrPhyCreature>(Goal.Object->GetOwner());
//если объект цели - другое существо
if (MyrGoal)
{
//домножить на средний размер меня и вас, чтобы точка касания примерно соответствовала единице, а не, например, 1/30см
DistFactor = (me()->GetBodyLength() + MyrGoal->GetBodyLength()) * 0.5;
//подсчёт общей заметности уже НАС для цели, которая могут быть агрессивными или которые нас боятся
//реально существо не может точно знать, что его видят, оно само должно хорошо видеть и/или слышать тех, чью зоркость оценивает
//к тому же существо должно быть уверенно, что оно отслеживает правильных оппонентов, для этого пока служит множитель Sure
if (Goal.EventMemory.GetRage() > 0.1f || Goal.EventMemory.GetFear() > 0.1f)
Paranoia += HowClearlyYouPerceiveMe(MyrGoal) * Goal.Sure();
}
else DistFactor = me()->GetBodyLength();
//собственно обратное расстояние, чтобы бесконечность читалась конечной точкой
DistFactor *= Goal.LookAtDistInv;
}
//цвет эмоции домножается на уверенность, чтобы действовал стелс
// пока враг не уверен, что его преследуем мы, к которому он плохо относится, он не делает резких определенных движений
// возможно, следует помещать уверенность в альфа-канал, чтобы поведение при неизвестном объекте было разным
auto myEmotion = Goal.EventMemory.Emotion * Goal.Sure();
//▄ получаем вектор аналоговых тяг для пограничных эмоций (положительный или отрицательный)
//* это отдельный параметр для цели, выбор движения осуществляется на его основе дискретным образом
const auto GainsForDist = me()->GetGenePool()->MotionGainForEmotions->GetUnadjustedLinearColorValue(DistFactor);
float RealGain = DotProduct(GainsForDist, myEmotion);
//▄ получаем условный аналоговый коэффициент желания быть незаметным
//* это аддитивный параметр, который аккумулируется по всем целям
const auto StealthForDist = me()->GetGenePool()->StealthGainForEmotions->GetUnadjustedLinearColorValue(DistFactor);
StealthAmount += DotProduct(StealthForDist, myEmotion) * Goal.Weight;
//▄ получаем условный аналоговый коэффициент надобности поворота к цели определенной частью тела
//* это аддитивный параметр, который аккумулируется по всем целям
const auto FacingForDist = me()->GetGenePool()->StealthGainForEmotions->GetUnadjustedLinearColorValue(DistFactor);
FacingAmount += DotProduct(FacingForDist, myEmotion) * Goal.Weight;
return RealGain * Goal.Weight;
}
//==============================================================================================================
//сложная трассировка (цель получает значение MoveToDir и, возможно, ClosestPoint)
//==============================================================================================================
ERouteResult AMyrAI::Route(FGoal& MainGoal, FGestaltRelation* Gestalt, FHitResult* WhatWeSaw)
{
//если по чувствам нас скорее тянет в сторону цели
if(Drive.Gain > 0)
{
//трассировка до цели обнаружила препятствие
if(WhatWeSaw->bBlockingHit)
{
//если препятствие - сам объект цели, то есть мы его успешно увидели
if(MainGoal.Object == WhatWeSaw->Component.Get())
{
//простейший случай - направить бег напрямую к видимой цели
MainGoal.MoveToDir = MainGoal.LookAtDir;
return ERouteResult::Towards_Directly;//◘◘>
}
//если препятствие - иной объект, который загораживает цель
else return DecideOnObstacle (MainGoal.LookAtDir, MainGoal, Gestalt, WhatWeSaw, MainGoal.MoveToDir);//◘◘>
}
//луч трассировки не дотянулся ни до цеди, ни до препятствия - вероятно, когда трассировка даже не проводилась
else
{
//идём вперед напрямую
MainGoal.MoveToDir = MainGoal.LookAtDir;//◘◘>
return ERouteResult::Towards_Directly;
}
}
//если нас тянет прочь от цели, а цель при это почему-то видима
else
{
//для начала просто переворачивает векторы, делаем тягу положительной
MainGoal.MoveToDir = -MainGoal.LookAtDir;
Drive.Gain = -Drive.Gain;
//пытаемся посмотреть назад от цели (вопрос пока на какой радиус вдаль рассчитывать)
FHitResult Hit2; FVector3f LookBckRadius = -MainGoal.LookAtDir * 100;
TraceForVisibility(MainGoal.Object, Gestalt, (FVector)LookBckRadius, &Hit2);
//если нашли препятствие, обходим
if(Hit2.bBlockingHit)
return DecideOnObstacle(-MainGoal.LookAtDir, MainGoal, Gestalt, &Hit2, MainGoal.MoveToDir);//◘◘>
else
return ERouteResult::Away_Directly;//◘◘>
}
}