Похожие презентации:
6. ОПВ ч.3 Джойстики 1
1.
Название программыUnity
2023
Тема занятия
Организация
пользовательского
ввода: Джойстики 1
Многоуровневое хранение данных ч.1
2.
2023План
занятия
Unity
Введение
Теория
К чему эти сложности?
Практика Оболочка
Домашнее задание
Итоги
Изображение от Storyset на Freepik.com
3.
2023Unity
Проверяем
домашнее задание
1.
Какие трудности возникли
при выполнении задания?
3.
2.
Как можно было бы
избежать трудностей?
4.
Какой информации не хватило
для выполнения задания?
Что больше всего понравилось/не
понравилось в процессе
выполнения задания?
4.
2023Вот как это
можно было сделать:
В классе InputManagerManipulator
создаем метод ChangeAxis,
который будет наличие оси и вносить
в нее изменения.
Unity
public static void ChangeAxis(EmptyAxis axis)
{
if (AxisDefined(axis.name) == false)
{
Debug.Log("Ось " + axis.name + " не найдена!");
return;
}
SerializedObject serializedObject = new
SerializedObject(AssetDatabase.LoadAllAssetsAtPath("ProjectSettin
gs/InputManager.asset")[0]);
SerializedProperty axesProperty =
serializedObject.FindProperty("m_Axes");
for (int i = 0; i < axesProperty.arraySize; i++)
{
SerializedProperty axisProperty =
axesProperty.GetArrayElementAtIndex(i);
5.
2023Unity
Введение
if (axis.name == GetChildProperty(axisProperty,
"m_Name").stringValue)
{
GetChildProperty(axisProperty,
"m_Name").stringValue = axis.name;
GetChildProperty(axisProperty,
"descriptiveName").stringValue = null;
GetChildProperty(axisProperty,
"descriptiveNegativeName").stringValue = null;
GetChildProperty(axisProperty,
"negativeButton").stringValue = axis.negativeButton;
GetChildProperty(axisProperty,
"positiveButton").stringValue = axis.positiveButton;
GetChildProperty(axisProperty,
"altNegativeButton").stringValue = axis.altNegativeButton;
GetChildProperty(axisProperty,
"altPositiveButton").stringValue = axis.altPositiveButton;
GetChildProperty(axisProperty, "gravity").floatValue =
axis.gravity;
GetChildProperty(axisProperty,
"dead").floatValue = axis.dead;
GetChildProperty(axisProperty,
"sensitivity").floatValue = axis.sensitivity;
GetChildProperty(axisProperty,
"snap").boolValue = axis.snap;
GetChildProperty(axisProperty,
"invert").boolValue = axis.invert;
GetChildProperty(axisProperty,
"type").intValue = (int)axis.type;
GetChildProperty(axisProperty,
"axis").intValue = (int)axis.axis;
GetChildProperty(axisProperty,
"joyNum").intValue = axis.joyNum;
serializedObject.ApplyModifiedProperties();
Debug.Log("Ось " + axis.name + " была
изменена!");
}
}
6.
2023Unity
Введение
public void MakeChange()
{
for (int i = 0; i <= AxesCount; i++)
{
EmptyAxis newAxis = new EmptyAxis();
В классе AxesCreator создаем метод
MakeChange, который является копией
метода CreateNewAxes,
но в конце вызывается метод
ChangeAxis, вместо AddAxis.
if (RemoveZeroFromName && i == 0) newAxis.name =
DeviceName + ButtonName + AxisName;
else newAxis.name = DeviceName + i + ButtonName +
AxisName;
string deviceNumber = i.ToString() + " ";
if (i == 0) deviceNumber = null;
if (InsertNegativeDeviceNumber)
newAxis.negativeButton = NegDevice + deviceNumber + NegButton;
7.
2023Unity
Введение
else newAxis.negativeButton = NegDevice +
NegButton;
if (InsertPositiveDeviceNumber)
newAxis.positiveButton = PosDevice + deviceNumber + PosButton;
else newAxis.positiveButton = PosDevice +
PosButton;
newAxis.gravity = Gravity;
newAxis.dead = Dead;
newAxis.sensitivity = Sensitivity;
newAxis.snap = Snap;
newAxis.invert = Invert;
newAxis.type = Type;
newAxis.axis = Axis;
if (InsertAltNegativeDeviceNumber)
newAxis.altNegativeButton = AltNegDevice + deviceNumber +
AltNegButton;
else newAxis.altNegativeButton = AltNegDevice +
AltNegButton;
if (InsertAltPositiveDeviceNumber)
newAxis.altPositiveButton = AltPosDevice + deviceNumber +
AltPosButton;
else newAxis.altPositiveButton = AltPosDevice +
AltPosButton;
newAxis.joyNum = i;
InputManagerManipulator.ChangeAxis(newAxis);
}
}
8.
2023Unity
Введение
public override void OnInspectorGUI()
{
AxesCreator creator = (AxesCreator)target;
В метод OnInspectorGUI добавляем вывод
дополнительной кнопки, которая вызывает
срабатывание метода MakeChange.
Готов!
Теперь оси можно редактировать!
if (creator.RemoveZeroFromName && creator.AxesCount == 0)
{
GUILayout.TextField("Axis name: " + creator.DeviceName
+ creator.ButtonName + creator.AxisName);
}
else
{
GUILayout.TextField("Axis name: " + creator.DeviceName
+ creator.AxesCount + creator.ButtonName + creator.AxisName);
}
9.
2023Unity
Введение
GUILayout.Space(20);
GUILayout.TextField("Negative Button: " + creator.NegDevice + negDevNumber +
creator.NegButton);
GUILayout.TextField("Positive Button: " + creator.PosDevice +
posDevNumber + creator.PosButton);
GUILayout.TextField("Alt Negative Button: " + creator.AltNegDevice
+ altNegDevNumber + creator.AltNegButton);
GUILayout.TextField("Alt Positive Button: " + creator.AltPosDevice
+ altPosDevNumber + creator.AltPosButton);
string negDevNumber = null;
string posDevNumber = null;
string altNegDevNumber = null;
string altPosDevNumber = null;
if (creator.InsertNegativeDeviceNumber && creator.AxesCount >
0) negDevNumber = creator.AxesCount.ToString() + " ";
if (creator.InsertPositiveDeviceNumber && creator.AxesCount >
0) posDevNumber = creator.AxesCount.ToString() + " ";
if (creator.InsertAltNegativeDeviceNumber && creator.AxesCount
> 0) altNegDevNumber = creator.AxesCount.ToString() + " ";
if (creator.InsertAltPositiveDeviceNumber && creator.AxesCount
> 0) altPosDevNumber = creator.AxesCount.ToString() + " ";
DrawDefaultInspector();
if (GUILayout.Button("Create Axes"))
{
creator.CreateNewAxes();
}
if (GUILayout.Button("Make Change"))
{
creator.MakeChange();
}
}
10.
2023Unity
Введение
На прошлом уроке мы создали инструмент,
который позволил нам быстро наполнять
Input Manager необходимыми
осями для джойстиков.
Его доработка остается на твоих плечах!
Сегодня же мы перейдем
к реализации ввода с джойстив.
Изображение от Storyset на Freepik.com
11.
2023Unity
Реализация будет разделена на 2 урока,
т.к. система ввода достаточно большая.
На первом (этом) уроке мы реализуем
большую часть системы ввода.
На втором (следующем) уроке мы не только
доработаем данную систему,
но и начнем внедрять ее
у нашей общей систем командного ввода!
Не будем терять время и приступим!
Изображение от Storyset на Freepik.com
12.
Название программыUnity
2023
Теория
К чему такие
сложности?
Многоуровневое хранение данных ч.1
Давай вспомним как в работает система
ввода с геймпада в большинстве игр.
А более конкретно: как работают
настройки управления для геймпадов.
13.
2023Unity
Теория
Для этого нам снова нужно включить
критическое мышление.
Давай зададим себе несколько обыденных
вопросов, которые нам необходимо будет
решить в процессе обучения.
Изображение от Storyset на Freepik.com
14.
2023Unity
Теория
В любой состоявшейся игре пользователь
может производить настройки управления так,
как ему будет удобно.
На каждое направление стика может быть
назначено отдельное действие.
А всего направлений у стика 4.
Но как это сделать, если стик имеет
только по 2 оси, и его нельзя
проверить на нажатие?
Изображение от Storyset на Freepik.com
15.
2023Unity
Теория
Пользователь установить перемещение
на кнопки A, B, X, Y, если он видит это нужным.
Как это реализовать, если изначально
направления считываются
в осях типа float?
Изображение от Storyset на Freepik.com
16.
2023Unity
Теория
Выстрелы из пистолета
в игре реализуются одиночными нажатиями.
Пользователь хочет сменить
кнопку выстрела с [B] на [RT].
Как проверить одиночное
нажатие на [RT], если это ось?
Изображение от Storyset на Freepik.com
17.
2023Unity
Теория
Это далеко далеко не все вопросы, которые
могут возникнуть во время реализации.
Но постараемся ответить заранее на всех.
Изображение от Storyset на Freepik.com
18.
2023Unity
Теория
Чтобы у пользователя была возможность
свободно распоряжаться любыми
манипуляторами на геймпаде требуется
создать систему ввода поверх Unity.
Необходимо реализовать
все уже существующие методы так,
чтобы они могли принимать
и возвращать удобные нам параметры.
Каждую ось необходимо разбить
на 2 отдельных направления,
которые будут возвращаться
только положительным значением.
Для проверки одиночных нажатий
и опусканий осей потребуется сравнивать
данные между игровыми кадрами.
19.
2023Теория
Все эти небольшие проблемы мы решим
буквально за 2 урока!
Приступим!
Unity
20.
2023Практика
Оболочка
Для работы нам понадобится всего 1 скрипт.
Создаем скрипт «JoystickInput»
Unity
21.
2023Unity
Практика
using System;
using System.Collections.Generic;
using UnityEngine;
Заходим внутрь и делаем класс
статическим, убирая наследование.
public static class JoystickInput : MonoBehaviour
{
Внутри класса все очищаем.
}
22.
2023Практика
Unity
{
Any = 0,
Joystick_1 = 1,
Joystick_2 = 2,
Joystick_3 = 3,
Joystick_4 = 4,
Joystick_5 = 5,
Joystick_6 = 6,
Joystick_7 = 7,
Joystick_8 = 8,
Joystick_9 = 9,
Joystick_10 = 10,
Joystick_11 = 11,
Joystick_12 = 12,
Joystick_13 = 13,
Joystick_14 = 14,
Joystick_15 = 15,
Joystick_16 = 16,
Ниже добавляем публичный enum
с номерами джойстиков.
using System;
using System.Collections.Generic;
using UnityEngine;
public static class JoystickInput
{
}
public enum JoystickIndex
}
23.
2023Unity
Практика
public enum JoystickIndex…
Еще ниже создадим другой
публичный enum, который будет служить
аналогом KeyCode для джойстиков.
Положительными значениями
обозначаем кнопки, а отрицательными
значениями обозначаем оси.
Значение None
будем расшифровывать отдельно.
public enum JoystickKeyCode
{
A = 0,
B = 1,
X = 2,
Y = 3,
LB = 4,
RB = 5,
Back = 6,
Start = 7,
24.
2023Unity
Практика
LT = -1,
RT = -2,
LS_Left = -11,
LS_Right = -12,
LS_Up = -13,
LS_Down = -14,
RS_Button = 9,
DPAD_Left = -31,
DPAD_Right = -32,
DPAD_Up = -33,
DPAD_Down = -34,
LS_Button = 8,
RS_Left = -21,
RS_Right = -22,
RS_Up = -23,
RS_Down = -24,
None = -100
}
25.
2023Unity
Практика
На этом работа
с внешними данными закончена.
Напомню: если тебе захочется изменять
код в зависимости от платформы
— используй условия компиляции.
#if PLATFORM_STANDALONE_WIN
public enum JoystickKeyCode
#elif PLATFORM_STANDALONE_OSX
public enum JoystickKeyCode
Смотреть
#endif
26.
2023Unity
Практика
Дальше мы работаем
только внутри класса JoystickInput.
Создадим внутри него 7 основных методов,
которые нам потребуется развить за 2 урока.
Методы GetSlope и GetSlopeRaw
являются аналогами GetAxis и GetAxisRaw,
но возвращают только положительное
значение в конкретном направлении.
public static class JoystickInput
{
public static float GetSlope(JoystickKeyCode key, JoystickIndex
joystick = JoystickIndex.Any, float deadZone = 0.2f)
{
return 0f;
}
public static int GetSlopeRaw(JoystickKeyCode key, JoystickIndex
joystick = JoystickIndex.Any, float deadZone = 0.2f)
{
return 0;
}
27.
2023Unity
Практика
public static bool GetKeyUp( JoystickKeyCode key, JoystickIndex
joystick = JoystickIndex.Any, float deadZone = 0.2f)
{
return false;
}
public static bool GetKey( JoystickKeyCode key, JoystickIndex
joystick = JoystickIndex.Any, float deadZone = 0.2f)
{
return false;
}
public static bool GetKeyDown(JoystickKeyCode key, JoystickIndex
joystick = JoystickIndex.Any, float deadZone = 0.2f)
{
return false;
}
public static bool AnyKey(JoystickIndex joystick =
JoystickIndex.Any, float deadZone = 0.2f)
{
return false;
}
public static bool AnyKeyDown(JoystickIndex joystick =
JoystickIndex.Any, float deadZone = 0.2f)
{
return false;
}
}
28.
2023Unity
Практика
Пример:
if(JoystickInput.GetSlope(JoystickKeyCode.LS_Left) != 0)
{
//Левый стик наклонен налево
}
if(JoystickInput.GetSlope(JoystickKeyCode.LS_Right) != 0)
{
//Левый стик наклонен направо
}
Т.е. этим методам нужно будет
передать JoystickKeyCode,
а они вернут значения от 0 до 1.
Остальные методы являются полными
аналогами методов из класса Input,
но для работы с JoystickKeyCode.
29.
2023Unity
Практика
Для этих методов обязательным аргументом
для работы является JoystickKeyCode.
Однако, мы добавили
и не обязательные аргументы.
Например, JoystickIndex и float deadZone.
Если мы не передадим методам
JoystickIndex то они должны будут вернуть
значение любого джойстика.
Изображение от Storyset на Freepik.com
30.
2023Практика
Если ты не знаешь
что такое мертвая зона (Dead Zone):
Мертвая зона — небольшая область вокруг
пересечения осей стика, в которой наклон этого
самого стика не регистрируется.
Мертвая зона позволяет не создавать лишнюю
информацию с контроллера игрока,
если его девайс слишком чувствителен.
Например, геймпады XBOX настолько точны,
что их стики практически никогда
не возвращаются в центр пересечения.
Unity
31.
2023Unity
Практика
Но для работы нам понадобится еще один метод,
который будет принимать JoystickKeyCode
и расшифровывать его в string.
Это нужно для того, чтобы мы могли легко
обращаться к осям в Input Manager.
public static class JoystickInput
{
public static float GetSlope(JoystickKeyCode key, JoystickIndex
joystick = JoystickIndex.Any, float deadZone = 0.2f)...
Создаем приватный, статический
метод GetJoystickCodeDecrypt.
public static int GetSlopeRaw(JoystickKeyCode key, JoystickIndex
joystick = JoystickIndex.Any, float deadZone = 0.2f)...
32.
2023Unity
Практика
public static bool AnyKey(JoystickIndex joystick =
JoystickIndex.Any, float deadZone = 0.2f)...
public static bool GetKey( JoystickKeyCode key, JoystickIndex
joystick = JoystickIndex.Any, float deadZone = 0.2f)...
public static bool AnyKeyDown(JoystickIndex joystick =
JoystickIndex.Any, float deadZone = 0.2f)...
public static bool GetKeyDown(JoystickKeyCode key, JoystickIndex
joystick = JoystickIndex.Any, float deadZone = 0.2f)...
private static string GetJoystickKeyCodeDecrypt(JoystickKeyCode
key)
{
return null;
}
}
public static bool GetKeyUp( JoystickKeyCode key, JoystickIndex
joystick = JoystickIndex.Any, float deadZone = 0.2f)...
33.
2023Unity
Практика
Метод будет возвращать 2ю часть
названия оси, к которому мы будет
подставлять ключевое слово «Joy»
и номер джойстика.
private static string
GetJoystickKeyCodeDecrypt(JoystickKeyCode key)
{
if(key == JoystickKeyCode.A)
{
return "_A";
}
else if(key == JoystickKeyCode.B)
{
return "_B";
}
else if (key == JoystickKeyCode.X)
{
return "_X";
}
else if (key == JoystickKeyCode.Y)
{
return "_Y";
}
34.
2023Unity
Практика
else if (key == JoystickKeyCode.LB)
{
return "_LB";
}
else if (key == JoystickKeyCode.RB)
{
return "_RB";
}
else if (key == JoystickKeyCode.Back)
{
return "_Back";
}
else if (key == JoystickKeyCode.Start)
{
return "_Start";
}
else if (key == JoystickKeyCode.LT)
{
return "_LT";
}
else if (key == JoystickKeyCode.RT)
{
return "_RT";
}
// См. следующий слайд
35.
2023Unity
Практика
Продолжение метода:
// Продолжение
else if (key == JoystickKeyCode.LS_Left)
{
return "_LS_Horizontal";
}
else if (key == JoystickKeyCode.LS_Right)
{
return "_LS_Horizontal";
}
36.
2023Unity
Практика
else if (key == JoystickKeyCode.LS_Up)
{
return "_LS_Vertical";
}
else if (key == JoystickKeyCode.LS_Down)
{
return "_LS_Vertical";
}
else if (key == JoystickKeyCode.LS_Button)
{
return "_LS_Button";
}
else if (key == JoystickKeyCode.RS_Left)
{
return "_RS_Horizontal";
}
else if (key == JoystickKeyCode.RS_Right)
{
return "_RS_Horizontal";
}
else if (key == JoystickKeyCode.RS_Up)
{
return "_RS_Vertical";
}
else if (key == JoystickKeyCode.RS_Down)
{
return "_RS_Vertical";
}
else if (key == JoystickKeyCode.RS_Button)
{
return "_RS_Button";
}
// См. следующий слайд
37.
2023Unity
Практика
Окончание метода:
// Продолжение
else if (key == JoystickKeyCode.DPAD_Left)
{
return "_DPAD_Horizontal";
}
else if (key == JoystickKeyCode.DPAD_Right)
{
return "_DPAD_Horizontal";
}
else if (key == JoystickKeyCode.DPAD_Up)
{
return "_DPAD_Vertical";
}
else if (key == JoystickKeyCode.DPAD_Down)
{
return "_DPAD_Vertical";
}
else
{
return null;
}
}
38.
2023Unity
Практика
Расшифровка осей готова!
Будь внимательнее, т.к. работа идет
с типом string!
Если ось не будет найдена в Input Manager,
то есть только 3 варианта:
Ось действительно не существует/не была создана.
Допущена ошибка в наименовании оси
на предыдущем уроке.
Допущена ошибка в возврата
названия данным методом.
Внимательнее!
Изображение от Storyset на Freepik.com
39.
2023Unity
Практика
Теперь активируем основные рабочие
методы, GetKey и GetSlope.
В методе GetSlope мы должны будем
разбить полярности оси на две
положительные и вернуть результат.
Однако, если пользователь захочет
воспользоваться методом GetKey,
передав туда например LS_Up, мы должны
возвращать результат типом bool!
А это значит что работа этих методов будет
пересекаться.
Сделаем это!
40.
2023Unity
Практика
Переходим в метод GetSlope.
Для начала проверим что пользователь
передает не пустой JoystickKeyCode.
public static float GetSlope(JoystickKeyCode key,
JoystickIndex joystick = JoystickIndex.Any, float deadZone =
0.2f)
{
if(key == JoystickKeyCode.None)
{
return 0f; // Пользователь передает пустой
JoystickKeyCode
}
if (key < 0)
{
// Это ось
}
else
{
// Это кнопка
}
Далее разобьем течение на 2 потока:
Ось — когда передаваемый
JoystickKeyCode меньше нуля.
Кнопка — в ином случае.
}
41.
2023Unity
Практика
if (key < 0)
{
int joyNum = (int)joystick;
string joyNumStr = joyNum.ToString();
if (joyNum == 0)
{
joyNumStr = null;
}
Если это ось — расшифровываем ее данные
и возвращаем значение типом float.
string axisName = GetJoystickKeyCodeDecrypt(key);
if (axisName == null) return 0f;
42.
2023Unity
Практика
if (value > MathF.Abs(deadZone)) return value;
else return 0f;
if (Input.GetAxis("Joy" + joyNumStr + axisName) != 0)
{
float value = Input.GetAxis("Joy" + joyNumStr + axisName);
if(key == JoystickKeyCode.LS_Left || key ==
JoystickKeyCode.RS_Left || key == JoystickKeyCode.DPAD_Left
|| key == JoystickKeyCode.LS_Down || key ==
JoystickKeyCode.RS_Down || key == JoystickKeyCode.DPAD_Down)
{
if (value < -Math.Abs(deadZone)) return
Mathf.Abs(value);
else return 0f;
}
else if (key == JoystickKeyCode.LS_Right|| key ==
JoystickKeyCode.RS_Right || key == JoystickKeyCode.DPAD_Right
|| key == JoystickKeyCode.LS_Up || key ==
JoystickKeyCode.RS_Up || key == JoystickKeyCode.DPAD_Up)
{
}
else if(key == JoystickKeyCode.LT || key ==
JoystickKeyCode.RT)
{
if (value > MathF.Abs(deadZone)) return value;
else return 0f;
}
else
{
return 0f;
}
}
else
{
return 0f;
}
}
else
{
// Это кнопка
}
43.
2023Unity
Практика
Как это работает?
Извлекаем номер джойстика (joyNum)
и переводим его в string.
Если номер равен 0 — убираем цифру из название
(считываем данные сразу со всех джойстиков).
Получаем расшифровку JoystickKeyCode
и проверяем что результат есть.
Объединяем данные, чтобы найти ось в Input Manager
и получить с нее данные.
Проверяем какое направление передал пользователь,
чтобы отделить отрицательное
и положительное значение оси.
Возвращаем положительный результат по направлению.
Изображение от Storyset на Freepik.com
44.
2023Unity
Практика
Если это все же ось
— перенаправляем данные методу GetKey.
При получении положительного ответа
(кнопка нажата) — возвращаем 1f.
При отрицательном ответе (кнопка
в свободном положении) — возвращаем 0f.
if (key < 0)
{
// Это ось
}
else
{
if (GetKey(key, joystick, deadZone) == true)
{
return 1f;
}
else
{
return 0f;
}
}
45.
2023Unity
Практика
Как ты уже понимаешь — метод GetSlope
не может работать без метода GetKey.
Поэтому давай это доработаем!
Изображение от Storyset на Freepik.com
46.
2023Unity
Практика
Метод GetKey будет существенно
меньше чем GetSlope.
Изначальная логика работы сходится
с GetSlope, но теперь мы меняем ее местами.
Если значение отрицательно
— перенаправляем данные в GetSlope.
В ином случае расшифровываем
и возвращаем полученные с оси данные
при помощи Input.GetButton.
public static bool GetKey( JoystickKeyCode key,
JoystickIndex joystick = JoystickIndex.Any, float deadZone =
0.2f)
{
if (key == JoystickKeyCode.None)
{
return false;
}
47.
2023Unity
Практика
else
{
int joyNum = (int)joystick;
if (key < 0)
{
float value = GetSlope( key, joystick, deadZone);
string joyNumStr = joyNum.ToString();
if (joyNum == 0) joyNumStr = null;
if (value != 0)
{
return true;
}
else
{
return false;
}
}
string buttonStr = GetJoystickKeyCodeDecrypt(key);
if (buttonStr == null) return false;
string axisName = "Joy" + joyNumStr + buttonStr;
return Input.GetButton(axisName);
}
}
48.
2023Практика
На сегодня это все!
На следующем уроке мы доработаем
работу данного класса
и наполним оставшиеся методы.
Unity
49.
2023Unity
Домашнее
задание
Реализуй работу методов GetSlopeRaw
и AnyKey в классе JoystickInput.
Аналогично методу GetAxisRaw,
метод GetSlopeRaw должен возвращать
округленное значение (0 или 1).
Метод AnyKey должен реагировать на
любое действие пользователя с джойстика.
Подсказка: для активаци AnyKey
потребуется перебирать
все JoystickKeyCode циклом.
50.
2023Unity
Итоги
1
Что нового сегодня было на занятии?
3
2
Зачем разбивать ось на два значения?
4
Что будет в этом случае:
if(JoystickInput.GetKey(JoystickKeyCode.RT))?
Что такое мертвая зона стиков и триггеров?
51.
2023Unity
Итоги
5
6
Зачем в JoystickKeyCode
мы разделили значения
на положительные и отрицательные?
Какое задание было сложным
на твой взгляд?
7
Как ты думаешь, где еще ты бы мог
применить эти знания?