Я впервые пишу здесь, поэтому прошу прощения, если не предоставил вам достаточно подробностей. У меня такое чувство, будто я схожу с ума, пытаясь сделать что-то, что, как мне казалось, будет таким простым и понятным! Для контекста: сейчас я пишу состояния поведения для вражеского охранника в проекте стелс-игры, над которым я работаю. Основные вещи, такие как патрулирование, расследование, бой и поиск. Я пытаюсь выяснить точный момент, когда игрок выходит из поля зрения противника. Затем в этом же кадре я захвачу положение игрока и сохранит его, чтобы враг мог обыскивать эту область.
Я не уверен, заключается ли моя проблема в недостатке знаний C#, или мне нужен перерыв или что-то в этом роде, возможно, комбинация того и другого, но, тем не менее, я чувствую, что уже должна быть система для этого, встроенная в Unity? Нет?
Мне нужен перерыв прямо сейчас, но я буду искать ответы и готов предложить свой код для помощи, если это будет полезно.
Спасибо!
(Редактировать): Вот мой код видения врага:
// Obstruction raycast check
if (Physics.Raycast(headObj.position, (player.transform.position - headObj.position).normalized, Vector3.Distance(headObj.position, player.transform.position), obstructionLayer) == false)
{
if (playerInVisionCone)
{
if (awareness < awarenessCombatThreshold)
{
awareness += Time.deltaTime * distanceAwarenessMultiplier;
}
}
// Enemy gains less awareness when looking through peripheral vision
// Enemy can't see player if they are crouching or sliding inside the peripheral vision cone
else if (playerInPVisionCone && !playerInVisionCone && !isCrouched && !isSliding)
{
if (awareness < awarenessCombatThreshold)
{
awareness += Time.deltaTime * pVisionAwarenessMultiplier * distanceAwarenessMultiplier;
}
}
// Player is not in vision cone of enemy
else if (!playerInPVisionCone && !playerInVisionCone)
{
if (awareness > 0)
{
awareness -= Time.deltaTime * awarenessCooldownMultiplier;
}
}
}
// Player is behind a wall
else
{
if (awareness > 0)
{
awareness -= Time.deltaTime * awarenessCooldownMultiplier;
}
}
ВЫ рисуете все это, верно? Разве вы не можете определить линию обзора для каждого нарисованного кадра?
@TimRoberts Да, но мне нужен точный кадр, в котором изменяется логическое значение. В этом случае, как только линия видимости нарушена, логическое значение будет истинным ровно для одного кадра, поскольку функция поиска выполняется только один раз. Затем в следующем кадре, даже если игрок все еще не находится в поле зрения, логическое значение должно быть ложным. Vector3 «lastKnownPos» должен быть позицией игрока в том же кадре, в котором линия обзора нарушена. Если функция запускается в каждом кадре, враг будет захватывать позицию игрока в каждом кадре, что приводит к тому, что у врага, по сути, есть «взломы стен». Если это имеет смысл
Но вы можете проверять это условие для каждого рисуемого кадра, верно? Это все под вашим контролем.
@ТимРобертс Да
Рассматривали ли вы возможность использования свойства вместо поля? В противном случае, что мешает вам использовать событие вместо проверки поля опросом?
Есть ли простой и эффективный способ проверить точный кадр, в котором логическое значение меняется с true на false или наоборот, а затем вызвать функцию?
Классический способ сделать это — сохранить значение в логическом поле, а затем сравнить новое значение со старым значением в следующем кадре:
bool _oldValue;
void Update() {
bool newValue = your calculations;
if (newValue != _oldValue) {
_oldValue = newValue;
YourFunction();
}
}
void YourFunction() { }
Это правильный способ сделать это, когда вся ваша логика находится в одном классе. Если в какой-то момент у вас появится много таких переменных и это станет беспорядок, вам следует рассмотреть возможность рефакторинга вашего кода на несколько классов.
Вы можете использовать свойство:
bool _value;
bool Value {
set {
if (_value == value)
return;
_value = value;
YourFunction();
}
}
void YourFunction() { }
void Update() {
Value = your calculations;
}
На первый взгляд этот подход кажется изящным, но проблема в том, что программисты обычно не ожидают побочных эффектов от свойств. Вызов функции, которая меняет что-то другое, является побочным эффектом. Вы все еще можете использовать этот подход до тех пор, пока свойство не будет открыто как public
. Но в реальном проекте, где вместе работают несколько программистов, лучше этого не делать.
Также возможно инкапсулировать значение в классе:
public class Value {
bool _value;
public bool HasChanged { get; private set; }
public Value(bool value = false) => _value = value;
public void Update(bool newValue) {
IsChanged = newValue != _value;
_value = newValue;
}
}
но, как вы можете видеть ниже, это само по себе не дает много преимуществ с точки зрения кода:
Value _myValue = new();
void Update() {
bool newValue = your calculations;
_myValue.Update(newValue);
if (_myValue.IsChanged) {
YourFunction();
}
}
Другими словами, введение такого универсального класса только для хранения и проверки переменной не имеет смысла. Это одинаковое количество строк кода, ясности это не добавляет и т.д.
Если в вашем коде есть несколько классов, вы можете начать использовать события. Это также правильный способ сделать это, когда некоторые API предоставляют вам события. Вот реализация простого события C#:
public class Value {
bool _value;
public event Action ValueChanged;
public Value(bool value = false) => _value = value;
public void Update(bool newValue) {
if (newValue == _value)
return;
_value = newValue;
ValueChanged?.Invoke();
}
}
И затем вы используете его следующим образом:
Value _myValue = new();
void OnEnable() => _myValue.ValueChanged += YourFunction;
void OnDisable() => _myValue.ValueChanged -= YourFunction;
void YourFunction() { }
void Update() {
bool newValue = your calculations;
_myValue.Update(newValue);
}
Этот код уже кричит, что часть your calculations
должна перейти в Update
класса Value
, поскольку теперь ясно, что именно этот класс принимает решения. Я не буду здесь углубляться в рефакторинг, но извлечение классов является распространенным инструментом. Также не забывайте переименовывать классы в соответствии с их назначением.
События также допускают параметры:
public class Value {
bool _value;
public event Action<bool> ValueChanged;
public Value(bool value = false) => _value = value;
public void Update(bool newValue) {
if (newValue == _value)
return;
_value = newValue;
ValueChanged?.Invoke(_value);
}
}
Value _myValue = new();
void OnEnable() => _myValue.ValueChanged += YourFunction;
void OnDisable() => _myValue.ValueChanged -= YourFunction;
void YourFunction(bool newValue) { }
void Update() {
bool newValue = your calculations;
_myValue.Update(newValue);
}
События C# также работают с MonoBehaviour
:
public class Foo : MonoBehaviour {
public event Action HasHappened;
}
public class Bar : MonoBehaviour {
[SerializeField] Foo _foo;
void OnEnable() => _foo.HasHappened += OnHappened;
void OnDisable() => _foo.HasHappened -= OnHappened;
void OnHappened() { }
}
Таким образом, вы можете сделать класс Value
классом MonoBehaviour
вместо того, чтобы отправлять все параметры в его Update
. Или вы можете использовать события для общения между вашими Enemy
и Player
классами. Распространенной реализацией является то, что один класс напрямую вызывает методы другого и подписывается на его события, чтобы получать от него ответ.
Кроме того, события C# не доступны инспектору. Для этого вы можете использовать Unity Events
Пока вы работаете в рамках одного класса, используйте подход, основанный на поле или свойстве. Как только задействовано несколько классов, начните изучать события и другие инструменты.
Ого, это вполне могло спасти мой проект. Большое спасибо, чувак! Я использовал первый пример в своем проекте, и он мгновенно устранил все мои проблемы. Я также смог немного изменить его, чтобы он лучше соответствовал моим потребностям, и он работает безупречно. Я оставлю эти два метода в своем репертуаре и планирую потратить пару часов или около того на изучение событий. Я раньше видел их использование в Интернете, но сам никогда в них не вникал. Будь счастлив, чувак!
Опубликуйте свой код. В противном случае мы не сможем вам помочь.