Как сделать марио в юнити
Все вроде бы нормально, спрайт персонажа движется хорошо и вправо и влево и подпрыгивает хорошо.
Но как только стоит подпрыгнуть при беге (неотжав кнопку бега) начинается полная ерунда!
Персонаж непрыгает и мало того нормально бежать перестает!
Движение персонажа
Пишу на андроид. Есть 3 кнопки, лево право, прыжок. Так вот, все три действия реализовываю с.
Добавлено через 33 секунды
Детский сад сразу пролистай, сам скрипт ниже.
В этом туториале показано, как сделать движения, как в Марио, а так же стрельбу, как в Марио: Advanced Unity 2D Platformer-Player Movement
А ещё там есть сползания по стенке, двойной прыжок, бег.
Курс можно легко найти в инете. Можно весь курс не скачивать, а скачать и открыть только финальный проект.
Cr0c, Да как то не откомпилил, ошибку пишет и все..
ошибка CS0019: Оператор `* 'не может быть применен к операндам типа` UnityEngine.Vector2' и `UnityEngine.Vector2"
cortexdeveloper, 8Observer8,
Сейчас посмотрю.
cortexdeveloper,
Хорошая статья, у меня в принципе почти все так же только у меня не UI-кнопки, а кнопки из спрайтов.
Движение кнопкой с правой стрелкой переписал так что не физическим движком силу добавляем, а меняем transform так вроде чуть получше откликается, но пропала инерция у персонажа, но дело и не в этом.
Дело в том что все так же когда персонаж бежит он неможет подпрыгнуть.
Это тестил на реальном устройстве, так как на компьютере мышкой нажать за один раз можно только на одну кнопку!
А это как бы не дело. (
Понял сейчас попробую.
Дело в том что все так же когда персонаж бежит он неможет подпрыгнуть.Народ я кажется понял в чем причина!
Я сейчас оттестил код такого вида под мобильное устройствоо:
Кинул на форму два спрайта и на каждом при нажатии написал подкрасится в серый цвет.
- Жму на один спрайт - серый
- Жму на второй - спрайт тоже серый
Жму сразу на два! Какой то один из них серый, автрой не жмется уже!
Я все к чему говорю что OnMouseDown неподходит для мобильных устройств!
Все так же как в компе нельзя мышью нажать на две какие либо кнопки в любой программе!
В устройстве думаю лучше использовать touch!
Тогда сразу нажмем две кнопки (два спрайта) движение и прыжок!
Это пока предположение я еще не тестил но думаю отработает правильнее код!
Помощь в написании контрольных, курсовых и дипломных работ здесь.
Движение персонажа
Как сделать так что б персонажа двигался в направлении камеры, вот моя попытка Turn =.
Движение персонажа
Здравствуйте! Как сделать движение и отражение персонажа. я сделала движение вправо-влево и сделала.
Движение персонажа 2д
Приветствую! Делаю 2д платформер. Правильно ли я делаю движение персонажа? Если делать так, то.
Движение персонажа 2d платформер
Здравствуйте! Нужно сделать движение персонажа в 2д платформере, пока что только влево/вправо.
Два мира
Magical Drop 3 (источник: Kazuya_UK)
Самая мудрёная часть создания игры-головоломки в Unity заключается в том, что игра не живёт в пространстве мира. Во всяком случае, живёт не полностью.
В этом её отличие от других жанров. Платформеры, например, почти полностью живут в игровом мире Unity. Transform игрока сообщает о его положении. Коллайдеры (или, в некоторых случаях, raycast) говорят, находится ли игрок на земле, ударяется ли об потолок или столкнулся с врагом. Даже если вы не используете внутриигровую физику, то всё равно, скорее всего, добавляете силу или указываете скорость Rigidbody, чтобы обеспечить распознавание столкновений без затрат.
Игры-головоломки совсем другие. Если в вашей игре нужно щёлкать мышью, то она наверно получает какие-то координаты в пространстве мира, но их обычно преобразовывают в ячейку сетки, которая полностью живёт в коде. Для этого есть понятная причина: гораздо проще создать логику для такой игры как Tetris или Dr. Mario, когда работаешь не с отдельными пикселями, а с блоками или тайлами.
Блоки «Тетриса» определённо не должны прилипать к стенкам стакана
На самом деле, в своих экспериментах я стремился как можно больше придерживаться пространства мира. Я использовал физику для определения «приземления» тайлов и передавал данные в двухмерный массив только для определения заполнения строки. Это казалось более безопасным: в конце концов, то, что происходит в игровом мире реально. Именно это видит игрок, поэтому если хранить данные тут, то нет риска рассинхронизации, правда?
Я ошибался. Как бы я ни пытался настроить систему, она просто не работала правильно.
Туториал Unity Plus, ссылку на который я дал выше, оказал мне огромную помощь. Как минимум, он показал, что правильным подходом был полный перенос логики из игрового мира в абстрактную структуру данных. Если вы ещё этого не сделали, то хотя бы вкратце просмотрите его, потому что в этой статье я расширю логику «Тетриса» до логики match-3.
Про создание платформера на Unity. Часть 4.1, злодейская
С прошлой статьи из этого цикла прошло достаточно времени (а с предшествующих ей статей — еще больше), поэтому самое время вернуться и рассказать вам как сделать самую коварную и злодейскую часть любой компьютерной игры — врагов. Оговорюсь заранее: здесь мы не будем рассматривать создание искуственного интеллекта для противников вашего персонажа. Посмотрим на то, какие типы противников наиболее распространены в платформерах и как их реализовать с помощью Unity.
Осторожно, под катом по-прежнему много гифок!
Дисклеймер: весь показанный в статье код не является образцом для подражания, примером идеального кода и прочих вымышленных и не существующих в природе вещей. Практики, примененные в статье, могут являться одними из множества решений конкретной проблемы. А могут и не являться.
Итак, поехали. Я выделил четыре основных типа объектов, которые могут так или иначе помешать герою вашей игры достигнуть цели:
1) Статичные (вращающиеся пилы, «смертельные» блоки итд)
2) Ходящие по платформам (грибы и черепахи из Super Mario Bros)
3) Летающие (вороны со второго уровня Ghosts and Goblins)
4) Стреляющие (Баужя из своего замка того же Марио)
Еще есть боссы, но они могут объединять в себе все указанные выше типы, поэтому на них останавливаться не будем.
1) Статичные препятствия.
Сделаем вращающуюся пилу. Для реализации такого «врага» нужны буквально пара вещей — спрайт пилы и скрипт, который будет ее вращать. Сказано — сделано.
Перетаскиваем спрайт на сцену:
Создаем новый скрипт (это, как обычно, очень просто)
И добавляем туда код, выглядящий примерно так:
В публичной переменной speed задается скорость вращения. Важно отметитить, что положительное значение вращает пилу против часовой стрелки, а отрицательное — по часовой.
Теперь, чтобы взаимодействовать с пилой, добавим на нее какой-нибудь коллайдер (подробно это описано в интереснейших предыдущих частях) и изменим тег объекта на какой-нибудь подходящий в данной ситуации.
Теги — хорошая штука. Мы можем назначить игроку тег player, врагам тег enemy, а стенам и полу — level. После этой нехитрой процедуры проверка того, с чем мы, например, столкнулись, будет происходить гораздо проще. А еще можно найти какой-нибудь один (или все, что есть на сцене) объект с определенным тегом. Делается это примерно так:
Массив всех объектов с заданным тегом можно получить используя метод FindGameObjectsWithTag — проще некуда.
Вернемся к нашим пилам и создадим новый тег для врагов в игре.
В скрипт персонажа добавим следующую проверку
Как видите, все элементарно: проверяем коллизии, проверяем тег того, с чем столкнулись. Если все плохо, перезагружаем уровень. Или отнимаем жизнь. Или что-нибудь в этом духе. Вы ведь играли в платформеры, правда?
Вот как все это выглядит в итоге:
Вот так, просто и быстро, мы создали первого врага — который, конечно, не враг, но вполне себе препятствие. Идем дальше.
2) Ходящие, ползающие и другие враги, перемещающиеся по платформам.
Неподготовленному читателю может показаться что этот тип врагов сложнее в реализации чем первый. Спешу успокоить — это совсем не так. Как и в прошлом случае, нам нужны какой-нибудь спрайт, коллайдер на нем, скрипт и платформа, по которой все это будет двигаться. К этому небольшому списку добавится только rigidbody2D, чтобы на врага действовала физика и можно было устанавливать ему скорость.
К сожалению, рисовать я не умею и моего творческого таланта хватило только на такого злодея:
Для его перемещения используем следующий скрипт
Задаем скорость врага и направление его движения (-1 — влево, 1 — вправо), которое можно менять при столкновении со стенами, к примеру. Дальше просто — устанавливаем горизонтальную скорость, равную произведению значения скорости и направления.
Забавный факт — если поставить у rigidbody2D галку fixedAngle, то враг будет ползти, а если убрать, то
«Но он смотрит вправо, а двигается влево!» — заметит внимательный читатель. Давайте пофиксим это и будем разворачивать спрайт соответственно направлению движения:
И научим разворачиваться при столкновении со стеной. Для этого сделаем на уровне пару стен с тегом wall и напишем обработку коллизий. Вот такую:
Теперь, когда все на месте, итоговый результат будет выглядеть вот так.
Останется только добавить врагу тег Enemy для того, чтобы он действительно стал опасен для нашего персонажа.
Подведем промежуточный итог. Мы разобрали как создаются два типа «врагов» в 2D-платформерах: статичные и перемещающиеся по уровню. Как видите, это действительно очень просто и базовая реализация занимает совсем мало времени.
В следующей части я расскажу как создать два остальных вида врагов — летающих и стреляющих.
Stay tuned — будет интересно!
Еще немного полезных ссылок
Загрузить Unity
Загрузить бесплатную или пробную Visual Studio
Стать разработчиком универсальных приложений Windows
Попробовать Azure бесплатно на 30 дней!
Project Setup
Let's get to it. We will start Unity and select New Project:
Now we can select the Main Camera in the Hierarchy and use the typical blue Background Color ( red=107 , green=140 , blue=255 ), adjust the Size and the Position so the game looks right later on:
Создание игры Match-3 в Unity
Несколько лет назад на SeishunCon я заново открыл для себя игры match-3. Я играл в Dr. Mario детстве, но такие более соревновательные игры, как Magical Drop, Bust-A-Move и Tokimeki Memorial Taisen Puzzle-Dama, сильно отличаются от неё.
В результате я осознал, как много нейтральных решений связано с созданием игры match-3.
На следующем джеме Ludum Dare я решил поэкспериментировать, но сначала за неделю до этого для разогрева попробовал разработать алгоритм «Тетриса», обнаруживающий и удаляющий линии. Мне очень помог этот туториал Unity Plus. [Прим. пер.: у меня ссылка не открывается. Если вы знаете, как решить проблему, напишите мне, я дополню статью.] Разумеется, алгоритм «Тетриса» для поиска заполненных рядов гораздо проще, чем алгоритм, выискивающий разнообразные сочетания совпадающих тайлов.
Если вы хотите изучить эти примеры кода в контексте, то зайдите в мой репозиторий Ludum Dare 30. (Для бесстыдной саморекламы я снова использовал эту логику для игры Shifty Shapes.)
The Art Style
In order to prevent Copyright issues we can't use the original Super Mario Bros. Sprites in this Tutorial. Instead we will draw our own Sprites and make them look similar to the original game.
Before we start, let's create a new Sprites folder in the Project Area:
This is where we will put all our Sprites (which is a fancy game development word for images).
Bushes and Clouds
Just like in the original Super Mario Bros. game we will also add a few bushes and clouds to the background. As usual, we will begin by drawing the images:
Note: right click each image, select Save As. and save them all in the project's Assets/Sprites folder.
We will use the following Import Settings for all the images:
Now we can drag the Sprites from the Project Area into the Scene and position them where we like them to be:
Since they are part of the background, it's important that we select all of them in the Hierarchy and then assign the Background Sorting Layer again:
Note: as mentioned before, the Background Sorting Layer makes sure that those objects are always drawn behind everything else. Or in other words, Mario will always be drawn in front of them.
Our Unity Super Mario Bros. game slowly starts to look like one!
Unity 2D Super Mario Bros. Tutorial
Welcome to one of longest and most exciting Tutorials on this website. Shigeru Miyamoto's masterpiece Super Mario Bros. was released in 1985 for the Nintendo Entertainment System and turned out to be one of the most popular video games of all time.
The game is a great example when it comes to good game design. In this Tutorial we will create a small test scene of the game and focus on the core mechanics as much as possible. We will use the Unity3D Engine and it will only take 90 lines of code.
As usual, everything will be explained step by step and as easy as possible so everyone can understand it.
Анимации
Игра уже работает, но пока она не понятна интуитивно, в основном из-за отсутствия анимаций. Тайлы исчезают, а затем появляются на нижних строках. Сложно понять, что происходит, если не следить внимательно.
Это тоже сложный момент. Игровые объекты всегда являются отражением состояния игры, поэтому тайлы постоянно расположены в сетке. Тайлы всегда занимают то или иное место: тайл может быть в строке 1 или 2, но никогда — в строке 1,5.
В чём заключается сложность? Нам нельзя одновременно манипулировать игровым полем и анимацией. Вспомните, как работает «Тетрис» или Dr. Mario — следующий тайл не падает, пока все тайлы на поле не «улягутся». Это даёт игроку короткую передышку, а также гарантирует отсутствие непредвиденных состояний и взаимодействий.
Кстати, при начале нового проекта я рекомендую создавать перечисление (enumeration) «игровых состояний». Мне никогда не приходилось писать игру, в которой не нужно было знать состояние игры: сам процесс игры, пауза, отображение меню, диалоговое окно, и так далее. Лучше всего спланировать состояния на ранних этапах разработки, таким образом можно сделать так, чтобы каждая написанная вами строка кода проверяла, должна ли она выполняться в текущем состоянии.
Признаюсь, что моя реализация неуклюжа, но в целом идея такова: при удалении или падении тайла мы задействуем изменение состояния. Каждый объект GameTile знает, как обрабатывать это изменение состояния, и, что более важно, знает, когда нужно сообщать игровому полю, что он завершил свою анимацию:
После завершения анимации удаления игра должна проверить наличие падающих тайлов:
После завершения анимации падения нужно проверить наличие совпадений:
Этот цикл повторяется, пока у нас не останется больше совпадений, после чего игра может возвращаться к своей работе.
Преобразование из поля в пространство мира
Как только я понял, что этот переход удобен, остальное было простым. Я создал класс GameTile, отслеживающий цвет, строку и столбец тайла, и на его основании обновлял положение тайла. Вот его сокращённая версия:
Тайлы в сетке
Заметьте, что в этом случае TileSize является константой, определяющей размер тайла в единицах Unity. Я использую тайлы размером 64x64 пикселя, а спрайт в Unity имеет разрешение 100 пикселей на единицу, поэтому TileSize оказывается равным 0.64. Также я использую постоянное смещение, чтобы середина поля 7x7 находилась в координатах 0,0 пространства мира, а левый нижний угол являлся тайлом 0, 0 в игровом пространстве.
Также я создал массив, определяющий игровое поле как статичное поле (static field) в классе Board. (Board сначала был статичным классом, а потом превратился в синглтон (singleton), потому что мне нужно было изменять значения в редакторе, поэтому он неуклюже сочетает в себе черты игрового объекта и статичного класса.)
В туториале Unity Plus двухмерный массив использовался для хранения целых чисел, я же решил хранить в этом массиве ссылки на мои объекты GameTile. Это позволило мне передавать данные от тайлов и к ним напрямую (как вы увидите позже), что упростило удаление тайлов и создание анимации.
При внесении изменений в состояние игрового поля мне нужно было просто пройти циклом по всему массиву поля и сообщить каждому тайлу, где он должен находиться:
Заметьте, что в каждом случае мы выполняем преобразование из абстрактного игрового пространства в пространство мира. Игровые объекты Unity не хранят сами важную информацию о состоянии игры, они всегда являются только отображением этого состояния.
Падение тайлов
Падающий тайл
Определённые изменения — например, падение тайла или удаление тайлов, в этом случае — оставляют тайлы без опоры, и этот случай нужно разрешить (конечно, если это требуется по правилам вашей игры). И на самом деле это довольно простой алгоритм.
Теперь мы проходим столбец за столбцом, а затем строку за строкой. Порядок здесь важен.
В каждом столбце мы проходим снизу вверх, пока не находим пустую ячейку. Затем мы помечаем её. Следующий найденный тайл мы просто смещаем вниз, в это положение, и добавляем единицу к индексу «пустой ячейки»:
После завершения нужно не забыть снова вызвать функцию проверки совпадений. Очень вероятно, что падающие тайлы создали пустые строки.
На самом деле, если в игре ведётся счёт, то это упростит отслеживание комбо или множителей очков. Все эти повторения падений и удаления блоков являются рекурсиями того первого вызова, запущенного действием игрока. Мы можем понять, сколько всего совпадений возникло после действия игрока и какое количество уровней «цепочек» потребовалось для каждого действия.
Распознавание и удаление совпавших тайлов
Удаление совпавших тайлов
Это самая хитрая часть. Наверно, именно ради этого вы читаете статью.
Горизонтальное совпадение (как в «Тетрисе») реализовать довольно просто: нужно всего лишь искать смежные тайлы в одной строке. Даже добавление горизонтальных или вертикальных совпадений (как в Dr. Mario) является просто вариацией этой темы. Однако отслеживание набора смежных тайлов в одновременно горизонтальном и вертикальном направлении потребует рекурсии.
При каждом действии, изменяющем игровое поле, мы запускаем проверку. Первое, что мы делаем — копируем весь массив поля в другой массив:
Зачем? Мы увидим позже, что так будет гораздо проще определить, какие тайлы мы проверяли.
Мы начинаем процесс с «грубого» перебора. Пройдём от ячейки к ячейке (сначала строки, потом столбцы), проверяя каждую ячейку. Для каждой проверки мы сбрасываем некоторые переменные, используемые для отслеживания проверки, а затем вызываем отдельную функцию (которую позже применим для рекурсии):
Давайте рассмотрим эту функцию TestTile:
Если функция обнаруживает, что ячейка имеет значение null, то пропускает её. Ячейка с null означает, что она или пуста, или мы уже тестировали её. (Именно поэтому мы скопировали её в отдельный массив — так проще произвольно манипулировать новым массивом.)
Если ячейка имеет значение, то мы делаем следующие действия. Во-первых, мы запоминаем её как «текущую» ячейку, ту, которая находится на вершине рекурсивной цепочки. Затем мы удаляем её из копии игрового поля, чтобы не проверять дважды. Также добавляем её в список (List), чтобы запомнить, сколько смежных тайлов одного цвета мы нашли.
Есть два состояния, которые могут возникнуть позже в рекурсии, но мы поговорим о них потом. Проверив ячейку, мы берём четыре ячейки вокруг неё и выполняем для них ту же проверку.
«Текущая» ячейка уже установлена, и это значит, что мы не на первом уровне рекурсии. В этих вызовах функций у нас есть три варианта для каждой ячейки.
Во-первых, ячейка может быть null, и это снова значит, что мы уже проверяли её, или она пуста. И в этом случае мы снова ничего не делаем.
Во-вторых, ячейка может не совпадать с «текущей» ячейкой. В этом случае мы не считаем её «проверенной». Наша рекурсия проверяет наличие одного набора смежных тайлов одного цвета. Только потому, что этот тайл не является частью текущего набора, не значит, что он не является частью какого-нибудь другого.
В-третьих, ячейка может быть того же цвета, что и «текущая» ячейка. В таком случае, она «проверена», поэтому мы устанавливаем ей значение null в копии игрового поля. Также мы добавляем её в List, который используем как накопитель. Это одно из состояний, которое мы пропустили в примере выше:
Функция продолжит выполнять рекурсию, пока не закончатся все варианты, добравшись или до пустой ячейки, или до конца поля. В этот момент мы возвращаемся в основной цикл грубого перебора для обработки результатов.
Если в накопителе есть больше трёх тайлов, то мы нашли совпадение. Если нет, то мы проверили один или два тайла, но нам не нужно выполнять никаких действий:
Здесь, как мы рассмотрим позже, я просто включаю анимацию. Простейший подход, однако, заключается в том, чтобы пройти циклом по нашему накопителю и вызвать DestroyObject для игрового объекта каждого совпадающего тайла. Так мы убьём двух зайцев одним выстрелом: избавимся от внутриигровых объектов и присвоим ячейкам в состоянии игрового поля значение null.
Pipes
The Sprite
Those green pipes are a big part of Super Mario Bros., so let's draw one:
Note: right click on the image, select Save As. and save it in the project's Assets/Sprite folder.
And here are the Import Settings for our pipe:
Note: even though it's a 32 x 32 pixel Sprite, we still use to the same Pixels to Units value all the time.
Now we can drag the Sprite from our Project Area into the Scene. We will add two pipes to the right side of the world:
Pipe Physics
The pipes should be part of the physics world, which means that Mario should not be able to walk right through them. Instead he should collide with them. All we have to do to make them part of the physics world is select both of them in the Hierarchy and then click Add Component->Physics 2D->Box Collider 2D in the Inspector. If we take a look in the Scene then we can see the green Collider box around each pipe:
We can see that the lower part of the pipe is a bit thinner than our Collider:
This is not that much of a big deal, but let's take our time and use a neat little trick to create a perfectly fitting Collider. One option would be to just use a Polygon Collider, which would have some performance downsides.
The more elegant solution (in this case at least) is to add yet another Box Collider to our pipe and then align the first Collider so it fits the upper part of the pipe and the second Collider so it fits the lower part of the pipe:
Now the pipe's physics are perfectly realistic. It's little adjustments like this that will make the game feel right later on.
Premium Tutorial
Enjoyed this preview? Become a Premium member and access the full Unity 2D Super Mario Bros. Tutorial!
All Tutorials. All Source Codes & Project Files. One time Payment.
Get Premium today!
… и обратно
В моей игре был единственный случай, когда нужно было выполнять преобразование из мира в игровое пространство: когда игрок щёлкал мышью на пустое пространство, чтобы бросить на поле тайл. Для этой задачи я создал большой коллайдер под всем игровым полем и прикрепил к нем следующий скрипт:
Вот, собственно, и всё. Заметьте, что в сущности в нём выполняется действие, обратное UpdatePosition(), где игровое пространство преобразуется в пространство мира.
Requirements
Knowledge
Our Tutorial does not require any special skills. If you are somewhat comfortable around Unity and heard about GameObjects, Prefabs and Transforms before, then you should be able to follow it without any problems.
Feel free to read our easier Unity Tutorials like Unity 2D Pong Game to get used to the engine.
Unity Version
Our Super Mario Bros. Tutorial will use Unity 5.0.0f4. Newer versions should work fine as well, older versions may or may not work. The free version of Unity 5 now comes with all the engine features, which makes it the recommended version.
The Stones
The Stone Image
Now we can select the image in the Project Area and then modify the Import Settings in the Inspector:
Note: a Pixels to Unit value of 16 means that the size of 16 pixels equals the size of one unit in the game world. We will use this value for all our textures. So if we want to position two 16 x 16 Sprites next to each other we can use positions like (1, 0), (2, 0) and so on.
Now we can drag the image from the Project Area into the Scene in order to add it to the game world:
Let's select the stone in the Hierarchy and change its position to (0, 0, 0) in the Inspector:
Note: we want to make a 2D game, so we don't really care about the Z coordinate. It will be 0 for all objects in our game (except for the Camera). In 2D games we only care about X (the horizontal coordinate) and Y (the vertical coordinate).
The Sorting Layer
Since we are in a two dimensional game world, there will be situations where several images are drawn on top of each other, for example when the Mario stands in front of a bush. We always want the bush to be in the background, so that Mario is drawn in front of it (otherwise we wouldn't see him).
Let's tell Unity that the stones are always supposed to be in the background. This is what Sorting Layer's are used for. We can change the stone's Sorting Layer if we take a look at the Sprite Renderer component in the Inspector:
Let's select Add Sorting Layer.. from the Sorting Layer list, then add a Background layer and move it to the first position like shown below:
Note: Unity draws the layers from top to bottom, hence whatever should be in the background will be at the top of the list.
Now we can select the stone again and assign our new Background Sorting Layer to it:
Stone Physics
Right now the stone is only an image, nothing more. It's not part of the physics world, Mario won't be able to walk on top of it or anything. We will need to add a Collider2D to make it part of the physics world, which means that things will collide with the stone instead of falling or walking right through it.
We can add a Collider2D by selecting Add Component->Physics2D->Box Collider 2D in the Inspector:
Now the stones are part of the physics world, it's that easy.
Adding more Stones
Now we can add more stones to our Scene. We will take a look over to the Hierarchy where we right click the current stone and then select Duplicate:
We will position the new stone at (x=1, y=0):
Let's repeat this work flow until we have two rows of 21 stones (one row at y=0 and one below at y=-1). The important part is to always position them at rounded coordinates like (2, 0) and never at (2.003, 0.005). Here is how it looks if we press Play:
Читайте также: