16

Кодовый парусный корабль #2

Всем привет, мой предыдущий по этой теме не был оценен сообществом, потому что он в нем даже не появился. Чтобы понять о чем будет идти речь в этом посте рекомендую сначала ознакомиться с первым: http://pikabu.ru/story/kodovyiy_parusnyiy_korabl_5082490


Сразу ссылка в гит с самыми актуальными файлами для тех, кому не интересны пояснения: https://gitlab.com/open_sourse/pirate (надеюсь, ссылка будет работать)

Итак, в этой части мы объявим несколько простых и понятных методов и научим наш корабль передвигаться и стрелять. Итак, начнем с рассмотрения дополненного класса пушки, который был создан в предыдущем посте:

Кодовый парусный корабль #2 Код, Программирование, Unity, Гифка, Длиннопост

Первое: мы добавили булевую переменную charged, что в переводе означает "заряжено". Тут все просто, если пушка заряжена, то мы можем стрелять, а если нет - нет. Также можно увидеть пример конструкторов, которые я забыл добавить в предыдущем посте. Помимо этого были добавлены некоторые новые переменные в класс Ship, но их можно найти в гит и посмотреть.

Рассмотрим метод Fire, который возвращает класс Bullet (пуля, ядро - рассмотрим ниже). Опишем его простыми словами: если пушка не заряжена, то не возвращать ничего, а если пушка заряжена, то зарядить её и отдать экземпляр класса Bullet, передав в него урон пушки.

Кодовый парусный корабль #2 Код, Программирование, Unity, Гифка, Длиннопост

Вот класс нашего ядра. Тут есть поле урона и скорости (скорость полета пока статична). Ну и конструктор. Ничего интересного, в целом.

Итак, перейдем к нашему основному классу, на который мы потратим большинство содержания поста. К слову, класс называется Ship, однако, как вы уже могли заметить ранее, такой класс уже есть, но это совершенно разные классы, ибо путь к ним выглядит так:
1) App.Items.Ship.Ship
2) UniApp.Items.Ship.Ship

Ниже мы рассмотрим второй вариант (тут сразу добавлен вид скрипта в инспекторе):

Кодовый парусный корабль #2 Код, Программирование, Unity, Гифка, Длиннопост
Кодовый парусный корабль #2 Код, Программирование, Unity, Гифка, Длиннопост

Первое что мы видим, что данный класс имеет в себе поле App.Items.Ship.Ship, т.е. данный корабль должен принимать в себя общий класс корабля.


Дальше мы видим два списка с векторами. По названию можно понять что это позиции пушек. Они нужны для того, чтобы наши ядра вылетали с относительно реальных точек. Если бы корабль был в 3д, то вместо этих позиций находились реальные модели, хотя мы и сейчас могли добавить реальные модели, но в виде рисунков, ноооо...их нет. Почему? Поймете позже, когда увидите наш облавок.


Дальше идут листы с пулями - для чего они нужны увидите позже. Также есть экземпляр пули из которого будут скопированы остальные пули (в инспекторе просто добавлено в это поле префаб пули). И булевая переменная moving, т.е. - движение. Также увидите позже для чего он нужен.


(ВНИМАНИЕ, это абзац, как и все сущности в нем созданы для отладки и потому будут удалены) Итак, первое что мы делаем при старте игры собираем наш корабль из его составных частей. В предыдущем посте, наверное, была написана суть этого действия, но это не точно.


Также мы добавляем 3 пушки на правую сторону корабля и 5 пушек на левую (для показательности).

Итак, перейдем к основному, что мы должны рассмотреть в данном после - методы.

Кодовый парусный корабль #2 Код, Программирование, Unity, Гифка, Длиннопост

Первый метод - Fire. Делает понятную функцию, открывает огонь. К слову, сейчас я назначил клавиши для отладки какие-нибудь, однако позже это будет переработано так, чтобы совершенно не касаться этих методов, если мы захотим изменить управление.

Итак, метод разделен на две части: одна срабатывает если нажата кнопка Е, вторая - если Q. Почему так? Один вариант стреляет с левой...мачты...или как это вообще называется? С левой стороны, короче. А второй с правой, потому что пушки у корабля с разных сторон.

Если нажали E, то для каждого элемента листа с правыми пушками, объявленному выше, мы включаем функцию FirePreprocess. FirePreprocess делает следующее: для пушки запускаем метод Fire (App.Items.Ship.Cannon) и если метод возвращает null (а как мы помним, он возвращает null только когда пушка не заряжена), то ничего не делаем, а если возвращает что-то другое (App.Items.Ship.Bullet), то добавляем его в лист ядер, которые мы принимаем из аргументов функции.

Если переменная в методе начинается с _ (нижнего подчеркивание), то это значит, что данная переменная получается из аргументов функции.


Ну что же идем дальше, надеюсь, уже не забыли о чем мы говорили выше, ибо мне пришлось перечитывать, чтобы продолжить рассказ. Итак, когда мы для каждой пушки запустили метод FIrePreprocess, то дальше запускается метод FireProcess, которые берет нужный список ядер и просто создает новое ядро из префаба, объявленного в полях класса и рассматриваемого выше. Рассмотрим метод по строкам, ибо некоторые действия могут быть непонятны:
57. Создаем из префаба ядро, задав в качестве стартовой позиции позицию пушки, которую мы берем из листа позиций пушек.

58. Задаем новому ядру родительский объект, который является нашим кораблем. (это делается для того, чтобы ядро перешло в локальную систему координат нашего корабля).
59. Получаем компонент Bullet у нового объекта (к слову, это не тот Bullet, который был описан выше. Код этого Bullet будет ниже) и устанавливаем ему направление, которое мы принимаем из аргумента функции (там вправо или влево, в зависимости от того из каких пушек мы стреляем).

60. передаем класс App.Items.Ship.Bullet в UniApp.Items.Ship.Bullet.

61. И теперь самое интересное: мы убираем родителя у нового объекта. Логичный вопрос: а на кой черт мы его до этого назначали, а теперь убираем при чем в том же методе? Суть в том, чтобы сначала перевести объект в локальные координаты родителя и назначить ему вектор движения в зависимости от текущего местоположения корабля, а потом убрать привязку к координатам, чтобы движение корабля не влияло на местоположение ядра и, в целом, стало независимо ни от кого.
64. Очищаем лист, ибо мы уже создали все нужные объекты из него.

Фух, жуть, казалось такие простые методы, но объяснить их не так-то и просто, как кажется.
Ах да, обещанный скрин класса UniApp.Items.Ship.Bullet:

Кодовый парусный корабль #2 Код, Программирование, Unity, Гифка, Длиннопост

Тут есть два поля, которые мы обсудили выше и в функции Update(которая вызывается каждый кадр). Если пуля не назначена в классе, то мы ничего не делаем, а если назначена, то двигаем объект. Зачем нужна проверка на null Bullet-а? Все дело в том, что если класс наследуется от MonoBehaviur, то мы не можем создавать его экземпляры с помощью ключевого слова null, поэтому мы не можем создавать конструктор класса, чтобы гарантировать то, что данное поле будет назначено точно при создании.

Итак, к самому методу. Как вы можете видеть, мы берем направление (только X координату, ибо нам нужно перемещать объект только по X. Там в целом, есть еще свои причины почему сделано так, но не буду распыляться, ибо пост довольно большой выходит. Если интересно отвечу в комментах) умножаем его на скорость и дельта переменную времени, чтобы все было плавно.

Позже тут будет добавлен разгон ядра и его уничтожение, когда он пройдет свой максимальный путь.


Итак, вот как выглядит наша стрельба (этот черный кусок обгорелой деревяшки и есть наш корабль, извините, но рисовать не умею и знакомых, которым было бы интересно что-то рисовать для меня, нет):

Кодовый парусный корабль #2 Код, Программирование, Unity, Гифка, Длиннопост
Кодовый парусный корабль #2 Код, Программирование, Unity, Гифка, Длиннопост

К слову, я похоже неправильные пушки назначил, потому что снаряды вылетают с неправильной стороны, но это легко поправить.

Рассмотрим методы движения:

Кодовый парусный корабль #2 Код, Программирование, Unity, Гифка, Длиннопост

Первый метод Move двигает наш корабль ровно прямо на максимальной скорости (позже будет добавлен разгон) и изменяем переменную moving на true. Это сделано для того, чтобы в дальнейшем можно было поворачивать корабль только когда мы двигаемся...ну, мне кажется именно так ходят корабля, т.е. не могут вращаться на месте(если корабль не весловой))


Второй метод Rotation: суть первой строки только что описали, дальше если нажата A, то вращаем корабль по своей оси в одну сторону, а если D, то в другую. И в конце переключаем moving в false. Таким образом у нас в любом случае moving не останется true, ибо данные методы в Update стоят так:

Кодовый парусный корабль #2 Код, Программирование, Unity, Гифка, Длиннопост

Тут Fire где угодно можно поставить, а вот Move и Rotation только в таком порядке, по описанным выше причинам.


Вот, к слову, как выглядит наше передвижение:

Кодовый парусный корабль #2 Код, Программирование, Unity, Гифка, Длиннопост

Ну на этом наши полномочия все. В следующем посте (если этот не соберет гору минусов), мы добавим:

1) Ограничение количества пушек на одной стороне корабля

2) Вылет снарядов с разной скоростью

3) Разгон снарядов при полете

4) Разгон корабля при движении

Ну и может что-то еще

Найдены возможные дубликаты

+3

А на какую аудиторию пост рассчитан? тех кто начал программировать с юнити?

Описывать поля - ну такое. Не знаю кому непонятно, что любой параметр объекта вроде заряжено/не заряжено, ограничений на поворот пушки нужно где-то хранить.

Конструкторы, там же параметры просто записываются в поля тоже очевидно.

Советую прочесть книжку по C#, то что видно сразу:

- публичные поля долой, используйте свойства.

- не стоит возвращать null из методов, вместо Bullet Fire(), лучше bool TryFire(Bullet out bullet), из-за этого пришлось даже метод FirePreprоcess городить, хотя достаточно ифа.

А то знаете NullPointerException - это ошибка на миллиард https://www.infoq.com/presentations/Null-References-The-Bill....


И по логике

почему нельзя одновременно стрелять с двух бортов?


А так успехов.

раскрыть ветку 14
0

Прежде всего для тех, кто хочет хотя бы посмотреть как выглядит разработка такого простого модуля как корабль (ну и для себя, чтобы получить советы и тд.). Я не могу однозначно утверждать какой уровень программирования у людей в сообществе, поэтому принял, что самый низкий. В дальнейшем не буду описывать простые вещи.


Про публичные поля чет не понял, как я тогда вообще буду с юнити работать?


Если возвращать bool из Fire, то останется ровно такой же метод, только проверка на false будет, а не null, но в целом да, вероятно переделаю, спасибо.

А стрелять с двух бортов можно одновременно. Нажмите две кнопки сразу и выстрелите. Вопрос лишь в том, а на кой черт нам стрелять сразу с двух бортов, если противник находиться лишь слева, например?

раскрыть ветку 9
0

С# - это ооп язык, у ооп один из трех китов - это инкапсуляция.

Тут подробно о плюсах свойств вместо полей

https://ru.stackoverflow.com/questions/197067/%D0%94%D0%BB%D...


По Fire вообще по красоте, без ифов. Если из пушки вылетает или 0 или 1 пуля, то почему не может быть (в будущем) 2 3 10?

IEnumerable<Bullet> Fire()

{

if(charged)

yield return new Bulet();

else

yield break;

}


//ship

public void Fire()

{

...

ship.rightCannons_L.ForEach (c => rightBullet_L.AddRange(c.Fire()));

...

});


Непонятно разделение на левые и правые пули.


Не получиться стрелять сразу в две стороны

if (Input.GetKey (KeyCode.E)) {

..........

} else if (Input.GetKey (KeyCode.Q)) {

.........

}

Второй иф выполнится только если первый ложный.

раскрыть ветку 8
0

Как минимум в процессе разработки нужны публичные поля, потому что они будут видны в инспекторе объектов.

раскрыть ветку 3
0

А че unity не умеет свойства на ходу вычислять?

раскрыть ветку 2
+2

Когда новую часть Корсаров ждать?

+2
Кстати, по поводу установки корабля в родители ядра, чтобы ядро создавалось относительно пространства корабля: лучше вычисляйте позицию дула пушки в world space и используйте ее.
раскрыть ветку 4
0

Где вы увидели там пушку, чтобы вычислять её позицию дула? Да и если бы был корабль в 3д и пушка отдельной моделью, то никаких проблем нет, но тогда бы я двигал ядро физикой, а не через код.

раскрыть ветку 3
+2
Дулом может быть и просто объект в коде, не в этом суть. Она в том, что вы можете вычислить математически нужную вам позицию, без установки родителя: взять локальную позицию корабля, взять вектора Right и Left у корабля в локальном пространстве, чтобы понять, что есть лево, а что право. Дальше, установив заранее параметры отступов пушек как между собой, так и между центром корабля (или краев корабля, или вообще просто взять значения Bounds), посчитать позиции пушек. Причем, считаем только один раз, но в локальном пространстве корабля, так как в local позиция пушек никогда меняться не будет. А в нужный нам момент переводим в мировое пространство.
раскрыть ветку 2
+2
Есть комментарии по вашему посту:
По стилю кода - https://habrahabr.ru/post/26077/ и https://msdn.microsoft.com/ru-ru/library/ms229002(v=vs.110).... . То есть "_" - обозначает, что поле приватное и используется только в начале именования, а не является параметром метода. Данные правила хоть и рекомендуемы, но их использование является хорошим тоном.
По использованию API Unity:
- как можно меньше используйте методы Update, FixedUpdate, Awake, Start и т.п., так как они вызываются через рефлексию, что крайне медленно
- кэшируйте ссылки: не используйте GetComponent<>() и FindObject(s)OfType()
- как можно меньше используйте Instantiate(): инстанцирование объектов - процесс затратный, размещение в памяти объекта всегда связано с большими издержками. Решение простое - кэшируйте объекты! Особенно такие, как ядра пушек
- используйте принципы, которые заложены в Unity - КОП (Компонентно-ориентированное программирование) - облегчите понимание связей в логике. Один класс, в котором и стрельба, и движение и еще куча всего реализовано - не есть хорошо

И, я совсем не понял, что вы имели в виду тут: " Все дело в том, что если класс наследуется от MonoBehaviur, то мы не можем создавать его экземпляры с помощью ключевого слова null, поэтому мы не можем создавать конструктор класса, чтобы гарантировать то, что данное поле будет назначено точно при создании." Несколько раз перечитал...
раскрыть ветку 4
0
- как можно меньше используйте методы Update, FixedUpdate, Awake, Start и т.п., так как они вызываются через рефлексию, что крайне медленно

Что это за бред? Все это кешируется на первом вызове, а может быть и при билде игры уже сделан кеш.

Попробуйте сделать свой аналог вызова какого то метода из кода и сравните производительность юнити Update и своего метода через рефлексию без кеша в цикле.


как можно меньше используйте Instantiate()

Это верно, но только если игра уже не маленькая (ну или если на мобилке). Так же какой смысл автору делать вложение с этим лишним кодом ибо статья не про это. Достаточно упомянуть в статье про эту тонкость.

-4

По стилю кода: с помощью _ можно объявлять приватные поля И аргументы функции, по моему мнению, потому что аргументы функции грубо говоря приватные поля функции, которые закрыты областью видимости, а добавление m_ - не увидел для себя особой разницы в понимании что с m_, что без него.
В принципе, вы также были должны сказать, что все поля с гетерами-сетерами являются функциями и их нужно было писать с большой буквы. :D

По юнити API:
- если эти методы используются через рефлексию, то значит они вообще не работают на вебгл, потому что там рефлексия не работает? + например, Start или Awake, я считаю, наплевать сколько раз вызывать, потому что они всего один раз вызываются.
- можно и кешировать ссылки, но тут объектов всего-ничего. Возможно переделаю.
- как кешировать объекты чет мне не понятно. Я не знаю сколько ядер будет, как минимум. Но почитаю про это.
- тут соглашусь, можно разделить.

Черт, ключевого слова new* должно быть. Вроде бы и перечитывал, но ошибка затесалась.

Мне кажется, что многие стесняются показывать свой код, потому что любой код можно назвать говнокодом, но спасибо за критику.

раскрыть ветку 2
+3
По стилю кода - вы можете писать так, как вам удобно, никто не запрещает, ссылки - только рекомендация. Но есть момент при работе в команде: у команды разработчиков всегда используется один стиль написания кода, и, в большинстве случаев, это является стиль, который рекомендует Microsoft. Так как вы создаете обчующие посты, то, ИМХО, стоит придерживаться общепринятого стиля. Ради хорошего тона:)
По поводу m_ или просто _ - куча споров на самом деле, но m_ - пришло из С++, и чаще всего (по крайней мере на моей практике) используют _ .
По поводу рефлексии в WebGl - она там работает, но с ограничениями: вот тут у меня мало опыта с данной платформой.
Если у вас множество объектов на сцене, и компоненты на них используют методы Awake или Start для инициализации, то будут задержки при загрузке уровней. Поэтому советуется инициализировать объекты только тогда, когда они реально нужны, ну а данные методы использовать крайне редко, когда без них не построить логику.
По кэшированию - почитайте про пул объектов - как раз для ядер)
Я дал только общие советы, и все же советую почитать статьи на Хабре и книги по Unity: вы можете открыть множество удивительных моментов для себя.
раскрыть ветку 1
0

"Разгон снарядов при полете" снаряды небось реактивные.

0

Напомнило наш старый студенческий проект со второго курса:

https://youtu.be/aW9tJxZao6s

раскрыть ветку 1
0

Хех, да, забавно. И так же как у меня при движение снаряды вылетают не совсем прямо. )

0

Может я не увидел ответа на мой вопрос, у автора есть законченные проекты?

раскрыть ветку 5
0

Нет, но если пост не интересен, то можно минус поставить. Есть сторонние проекты, в которых я участвуют в кач-ве джуна (но времени остается много, поэтому и решил посты начать писать).

раскрыть ветку 4
0

Если времени много, то советую учиться программированию а не написанию постов.

раскрыть ветку 3
0

В целом идея серии статей хорошая, но хочу сделать замечание.

У вас стиль кода не для unity ибо вы создаете классы с задаваемыми параметрами прям в коде. Не подходит этот метод потому, что у unity есть редактор, который позволяет задавать различные параметры прям в сцене.


Лучше делать "компонентами", как в unity уроках. А именно сделать классы Body, Sail, ... наследуемыми от MonoBehaviour. А дальше просто на пустой gameobject перестаскиваете эти классы и получаем "корабль". Вот примерный код:

public abstract class Item : MonoBehaviour { ... }
[System.Serializable]
public class Body : Item { ... }
[System.Serializable]
public class Sail : Item { ... }
public class Ship : MonoBehaviour {
     [SerializeField] private Body m_body;
     [SerializeField] private Sail m_sail;
     void Awake (){
          m_body = GetComponent<Body>();
          m_sail = GetComponent<Sail>();
     }
}

Либо если вам сложен такой подход, то можно сделать по другому. А именно не создавать объекты классов в коде ибо их создает сам едитор (я ведь не ошибся? ;)). И просто подобно первому подходу создаем пустышку gameobject, перетаскиваем класс Ship и получаем "корабль" и самое главное мы можем задавать параметры "деталей корабля" прям в коде.

примерный код:

public abstract class Item { }
[System.Serializable]
public class Body : Item {
     public float hp, armor, etc;
}
[System.Serializable]
public class Sail : Item {
     public float speed, blaBla;
}
public class Ship : MonoBehaviour {
     [SerializeField] private Body m_body;
     [SerializeField] private Sail m_sail;
     void Start (){
          Debug.Log("body hp: " + m_body.hp);
     }
}

Дальше можно почитать про ScriptableObject и создать asset'ы с заготовками "деталей корабля". Например у нас небольшая игра где всего по пять (пять корпусов, пять видов парусов, etc) и эти самые ScriptableObject заготовки работают как файлы с конфигами только внутри самого едитора.


Ну или вы все это знаете и специально сделали код максимально простым. ;)

раскрыть ветку 3
0

Зачем вы хотите усложнить код и полностью перевести его на рельсы юнити? Я специально основу пишу на чистом языке, потому что он более гибкий. Например, мне достаточно написать в коде new Sail(1,1,1) и вот у меня уже парус есть, а в случае с юнити мне нужно взять, создать новый объект руками, задать ему руками поля, где-то его хранить (значит создать какой-то storage), потом получить ссылку на место хранение у объекта и тд.

раскрыть ветку 2
0

Такое прикольно когда играешься один с кубиками. ;)

А что если нужно подбирать параметры для баланса? Это нужно развернуть VS, поменять значение, развернуть unity, подождать компиляции скрипта и после этого только посмотреть на результат. А если в команде есть гейм дизайнер, который делает уровни, баланс и при этом не шарит в кодинге, то работа на день будет делаться неделю.

раскрыть ветку 1
0
ниасилил(
раскрыть ветку 4
0

Там специально во втором абзаце ссылка в гит.

раскрыть ветку 3
0

А вы можете помочь c с#? Не могу понять как использовать массивы в интерфейсах. В инете все примеры сложные и реализует x+y,х*у и так далее

раскрыть ветку 2
Похожие посты
Возможно, вас заинтересуют другие посты по тегам: