Шукати в цьому блозі

Практичне керівництво розробки ігор в Gideros 3

Частина 3/4



назад

Зміст

*****************************************

  • Логіка гри
  • Реалізація головної ігрової сцени
  • Використання текстурних пакетів
    • Упаковка текстур
    • Використання текстурних пакетів всередині проекту
  • Використання фізики в Gideros
    • Створення фізичних тіл
    • Запуск світу world
      • Налаштування  кордонів world 
    • Взаємодія з фізичними тілами
    • Обробка колізій Box2D
  • Управління пакетами та рівнями
    • Визначення пакетів
    • Створення LevelSelectScene
    • Генерування екрану вибору рівнів
    • Переключення між пакетами
    • Створення класу GameManager
  • Логіка розблокування рівнів
  • Читання карти рівня
  • Завершення рівня


*****************************************

Логіка гри

Ось  теми, які ми розглянемо в цьому розділі:

  • Створення сцени для основної логіки гри
  • Навчитися використовувати фізику в Gideros
  • Використання Gideros OOП для управління різними типами ігрових об'єктів
  • Маніпулювання кількома рівнями та рівнями визначення
  • Створення сцен для вибору паків/модів і рівнів гри
  • Управління прогресом гри шляхом розблокування нових рівнів

Реалізація головної ігрової сцени

Головна сцена гри - це місце, де буде відбуватися вся дія. Це місце, де ми матимемо об'єкти, які підстрибують та попадають один одному, як у грі Машбол.
Зазвичай, коли у вас є уявлення про гру, ви не думаєте про кількість рівнів, пакетів, балів тощо.
Ви думаєте про головний геймплей. Ви повинні перейти прямо до основного геймплея і створити мінімальний життєздатний прототип, щоб зрозуміти куди рухатись далі:

  • Чи можна реалізувати гру, так як ви її уявили?
  • Чи гра може іграбельна взагалі?
  • Чи гра приваблива, весела, ви будете грати в таку гру особисто?
Інакше ви б витратили величезну кількість часу, лише щоб зрозуміти, що геймплей, який ви уявили, не дуже зручний.

Ось чому ми  почнемо з головного: логіки гри

В попередніх розділах ми створили   кнопку Start Game , яка повинна вести на сцену ігрового рівня. Тепер створімо цю сцену, як і раніше. Спершу створіть файл LevelScene.lua в каталозі scenes, потім створіть клас LevelScene, який слід успадкувати від Sprite. Тепер визначте його метод init, де ми додамо наше фонове зображення, центруючи його на екрані з 0,5 опорними точками прив'язки, як завжди.

LevelScene = Core.class(Sprite)
function LevelScene:init()
 local bg = Bitmap.new(Texture.new("images/bg.jpg", true))
 bg:setAnchorPoint(0.5, 0.5)
 bg:setPosition(conf.width/2, conf.height/2)
 self:addChild(bg)
end
Наостанок - -додаємо цю сцену до main.lua у sceneManager.

--визначити сцени
sceneManager = SceneManager.new({
 --сцена start  
 ["start"] = StartScene,
 --сцена about 
 ["about"] = AboutScene,
 --сцена options 
 ["options"] = OptionsScene,
 --сцена level 
 ["level"] = LevelScene,
})
Подібно до інших сцен, ми також повинні обробляти кнопку "back" Android, щоб повернутися до попередньої сцени. У цьому випадку може бути більше завдань, які ми хочемо зробити, перш ніж повернутися, наприклад, збереження прогресу в грі, та інше. Отже, давайте зробимо окремий метод LevelScene: back () і пов'яжіть його з кнопкою back Android.

Отже, в методі LevelScene: back (), тепер ми просто повернемося до попередньої сцени, це є StartScene.

function LevelScene:back()
 sceneManager:changeScene("start", conf.transitionTime, conf.transition, conf.easing)
end
Усередині методу LevelScene: init () ми будемо слухати подію KEY_DOWN і перевірити, чи натиснута кнопка була кнопкою "back", після чого ми визиватимемо метод LevelScene: back ().

self:addEventListener(Event.KEY_DOWN, function(event)
 if event.keyCode == KeyCode.BACK then
 self:back()
 end
end)

Використання текстурних пакетів

Оскільки це буде сцена, в якій ми матимемо багато різної графіки, а не всі вони можуть бути використані з самого початку, поговоримо  про упаковку наших текстур.
Так що таке упакована текстура? Як ви бачили на попередніх сценах, під час відображення зображень, ми просто вказали шлях до зображення в класі Texture і використовували його всередині Bitmap . Хоча це працює і може бути достатньо у деяких ситуаціях (особливо при створенні прототипу гри), у цього також є багато недоліків.

Недоліки не невикористування  текстурних пакетів:


  • Зоображення потрібно читати з файлу та завантажувати в пам'ять. Це операція вводу / виводу, яка може бути досить дорогою у деяких системах, і хоча Gidero зберігає текстури і не потребує читання та завантаження ще раз протягом деякого часу, це  не є ефективним рішенням завантажувати кожен файл окремо.
  • Крім того, ця операція не асинхронна, а це означає, що, якщо ви намагаєтеся завантажити велику картинку, воно тимчасово призупинить вашу гру, але навіть якщо малюнки невеликі, вони всеодно при кожному завантажені буде підтормажувати гру .
  • Існує також різниця в тому, як ці текстури обробляються при відображенні. Графічний процесор набагато простіше обробляти одну велику текстуру, ніж перемикати між декількома маленькими текстурами, отже, це працює більш ефективно, коли ми використовуємо меншу кількість файлів.
Отже, для всіх цих трьох пунктів існує одне рішення - упакувати всю вашу графіку в єдиний файл, який міститиме всю необхідну графіку. Після завантаження цього пакунка відображаються лише окремі регіони цілого пакета як текстури, надані класу Bitmap.
Звичайно, як завжди, цей підхід має свої недоліки.

Завантаження одного більшого файлу на старті призведе до більш тривалого початкового завантаження, але воно все ще краще, ніж невеликі відставання протягом всієї гри при завантаженні кожного файлу окремо.

Крім того, вам може бути спокуса упакувати всі текстури всієї гри в один пакет, насправді погана ідея. Це може здатися більш ефективним, але в кінці кінців вам потрібна пам'ять, щоб зберегти все це і пам'ятаю, що ми створюємо мобільну гру, і, як правило, мобільні пристрої мають обмеження на цю пам'ять,тому ваша гра призведе до аварій на всіх пристроях, що не мають достатньо пам'яті.
Ось чому найкращий спосіб - взяти середину. Я зазвичай намагаюся розділити текстурні пакети між сценами, створюючи новий пакет текстур для кожної сцени. Якщо є графіка, яка часто повторюється між сценами,
Я зазвичай створюю один глобальний пакет, який буде використовуватися у всій грі. Я вважаю, що найкращий підхід до управління графікою в Gideros

Упаковка текстур


Отже, як  упаковати бакато малюнкі в один, а потім використовувати лише частину цієї текстури?   Gideros, це полегшує, по-перше, за допомогою GiderosTexture Packer, щоб об'єднати всю графіку в один малюнок, а потім, використовуючи клас TexturePack, щоб використовувати їх усі окремо у вашій сцені.
Отже, спробуємо це. Спочатку перейдіть до каталогу проекту (не в Gideros Studio, а в папку проекту на своєму комп'ютері) і створіть папку з назвою texturepacks. Саме тут ми будемо зберігати наші упаковані текстури та їхні проекти. У каталозі texturepacks створіть інший каталог з ім'ям sources..
Ми будемо там  зберігати непаковані малюнки, які ми там упаковували. Ми не будемо використовувати sources в самому проекті Gideros, але нам може знадобитися це, якщо ми зхочемо переробити упаковану текстуру, додаючи, наприклад, нові графічні файли або замінивши існуючі.
Як тільки це буде зроблено, скопіюйте зображення, які потрібно об'єднати в папку sources в каталозі texturepack. У нашому випадку це будуть зображення ігрових елементів і кнопок, які використовуються на сцені гри.
Потім перейдіть до каталогу в який інстальовано Gideros і відкрийте програму GiderosTexturePacker.

У вікні Texture Packer натисніть меню File у верхньому лівому меню та виберіть New Project; з'явиться діалогове вікно " New Project". Давайте назвемо проект LevelScene і збережемо його в каталозі texturepacks у  папці проекту Gideros. Ви можете зняти прапорець біля пункту Create directory for project  , оскільки ми можемо зберігати всі текстурні пакети в одній і тійже папці.

Коли це буде зроблено , натисніть кнопку OK.

Texture Packer, досить схожий на Gideros Studio, клацніть правою кнопкою миші на LevelScene на панелі Project та виберіть « Add Existing Files ...». Потім перейдіть до свого проекту Gideros в texturepacks\sources, в якому ви зберегли всі зображення, і виділіть їх усі, і натисніть кнопку "Open. ".

Texture Packer автоматично об'єднує ваші зображення в найменшій і найближчій розмірності розміру 2 ^ n (ще один трюк, щоб зробити обробку текстур більш ефективною). Приклад упакованої текстури показаний на наступному скріншоті:

Тепер збережіть проект, натиснувши File і вибравши « Save Project». Потім нам потрібно експортувати об'єднану текстуру, натиснувши File і вибравши Export Texture ..., вона повинна запропонувати ту ж саму директорію, в якій ми вирішили зберегти наш проект в каталозі texturepacks. Якщо це не так ви повинні змінити його на каталог texturepacks свого проекту Gideros і натиснути кнопку «Save », щоб експортувати текстуру.

Тепер у вас має бути три файли в каталозі texturepacks, що називаються
LevelScene.png, LevelScene.txt і LevelScene.tpproj. Файл LevelScene. tpproj містить проект текстуро пакера, і якщо ви хочете відредагувати упаковану текстуру, вам слід відкрити Texture Packer і відкрити цей файл проекту.

Інші два файли (LevelScene.png та LevelScene.txt) - це ті, що нам треба включити в наш проект Gideros. Файл LevelScene.png містить самі комбіновані текстури, і LevelScene.txt містить інформацію про те, як відокремити ці упаковані малюнки.

Використання текстурних пакетів всередині проекту

Отже, тепер, коли у нас є текстурпак,  Спочатку нам потрібно створити каталог texturepacks у програмі Gideros Studio і додати до цієї папки обидва файли (LevelScene.png та LevelScene.txt).

Потім завантажимо їх за допомогою класу TexturePack і присвоїм змінній  self.g,  . Додайте наступний рядок  коду до методу LevelScene: init ():
self.= TexturePack.new("texturepacks/LevelScene.txt", "texturepacks/LevelScene.png", true)
 Ввесь пакет текстур зберігається всередині self.g змінної, і ми можемо використовувати ці текстури, це схоже на те, як ми використовували окремі графічні файли. Давайте створимо кнопку restart, яка просто повернеться назад на цю сцену і перезапустить нашу гру.
Отже, спочатку ми створюємо об'єкт Bitmap, викликаючи self.g:getTextureRegion і вказуємо назву зоображення яке  знаходиться в текстурпаку. Це створить об'єкт Bitmap з текстурою зоображення. Це  , ми посилаємося на текстури, упаковані в один пакет з тим же ім'ям файлу, який вони мали в каталозі sources.
Потім ми створюємо екземпляр Button і надаємо йому Bitmap  .
Потім ми розташовуємо цю кнопку в нижній лівій частині екрана, як ми робили раніше, використовуючи зміщення позиції, що зберігаються в змінних config.dx та config.dy, і додаєм цю кнопку до нашої сцени.
Останнє, що нам потрібно зробити, - це додавання події, яка відкриватиме цю сам сцену при натисканні кнопки.
мітка

local restart = Bitmap.new(self.g:getTextureRegion("restart.png"))
local restartButton = Button.new(restart)
restartButton:setPosition(-conf.dx, conf.dy + conf.height - restartButton:getHeight())
self:addChild(restartButton)
restartButton:addEventListener("click", function()
 sceneManager:changeScene("level", conf.transitionTime, conf.transition, conf.easing)
end)
І ми маємо кнопку restart. Давайте також впровадимо кнопку menu, яка призупинить гру та відкриває меню .
Спочатку введемо метод LevelScene: createMenu (), який створить наш об'єкт меню і поверне його.

function LevelScene:createMenu()
end
Отже, в цьому методі ми спочатку затемнимо екран, використовуючи напівпрозорий чорно-заповнений об'єкт Shape. Аналогічно тому, що ми робили раніше , ми просто створюємо екземпляр Shape, визначаємо його заповнення як чорний напівпрозорий колір і  намалюєм, викликаючи метод beginPath ().
Тпри малюванні  незабуваємо про зсув кутів, щоб затемнявся ввесь екран без пропусків.

local menu = Shape.new()
menu:setFillStyle(Shape.SOLID, 0x000000, 0.5)
menu:beginPath()
menu:moveTo(-conf.dx,-conf.dy)
menu:lineTo(-conf.dx, conf.height + conf.dy)
menu:lineTo(conf.width + conf.dy, conf.height + conf.dy)
menu:lineTo(conf.width + conf.dy, -conf.dy)
menu:closePath()
menu:endPath()
Далі, оскільки ми хочемо створити цілком окремий шар, ми хочемо відключити всі можливі події  миші та сенсора під затемненним шаром, щоб вони не вплинули на гру.  Нам потрібно додати зупинку поширення події викликаючи метод  e:stopPropagation()

--disable input events
menu:addEventListener(Event.MOUSE_DOWN, function(e)
 e:stopPropagation() end)
menu:addEventListener(Event.MOUSE_MOVE, function(e)
 e:stopPropagation() end)
menu:addEventListener(Event.MOUSE_UP, function(e)
 e:stopPropagation() end)
menu:addEventListener(Event.TOUCHES_BEGIN, function(e)
 e:stopPropagation() end)
menu:addEventListener(Event.TOUCHES_MOVE, function(e)
 e:stopPropagation() end)
menu:addEventListener(Event.TOUCHES_return menuEND, function(e)
 e:stopPropagation() end)
Врешті-решт, нам  потрібно повернути створене меню

return menu

Тепер, коли ми створюємо шар меню, давайте визначимо метод, який його відкриє.

У цьому методі ми встановимо прапорець self.paused на значення true, щоб вказати, що гра призупинена. Потім ми перевіримо, чи властивість self.menu ще не визначено, і якщо це не так, ми створимо новий шар меню та збережемо його у властивості. Тоді ми просто додамо його на сцену

У цьому випадку нам потрібно лише один раз створити рівень меню і повторно використовувати той самий шар меню, який ми зберегли у self.menu.

function LevelScene:openMenu()
 self.paused = true
 if not self.menu then
 self.menu = self:createMenu()
 end
Тепер, використовуючи метод LevelScene: init (), давайте додамо кнопку, щоб відкрити меню. Ми отримуємо графічне меню з текстурпаку і створюємо з нього екземпляр класу Button. Потім ми розташовуємо його в нижньому правому куті, використовючи зміщення conf.dx та conf.dy, і додаємо подію, щоб відкрити меню натисканням.

local menu = Bitmap.new(self.g:getTextureRegion("menu.png"))
local menuButton = Button.new(menu)
menuButton:setPosition(conf.dx + conf.width - menuButton:getWidth(), conf.dy + conf.height - menuButton:getHeight())
self:addChild(menuButton)
menuButton:addEventListener("click", function(self)
 self:openMenu()
end, self)
Ви можете спробувати і запустити проект, і він повинен відкрити напівпрозорий чорний фон накладання на весь екран.

Тепер ми можемо додати кнопки до цього шару меню. Давайте просто створимо кнопку resume, але ви також можете додати кнопку restart, і back  на основновне меню StartScene тощо.

В події кнопки "resume", нам доведеться зняти гру з паузи  і приховати накладене меню. Отже, давайте визначимо метод LevelScene: closeMenu () спочатку, де ми просто знімаємо накладення self.menu із сцени та змінюємо  self. paused прапорець на false. .

function LevelScene:closeMenu()
 self:removeChild(self.menu)
 self.paused = false
end
Тепер ми можемо додати кнопку Resume в методі LevelScene: createMenu (), перед тим як повернути об'єкт меню. Ми створюємо зображення з текстурпаку та передаємо його в наш екземпляр класу Button, після чого ми розташовуємо цю кнопку і додаємо подію, щоб викликати метод closeMenu (). Ми додаємо цю кнопку до нашого шару menu.


--додати кнопки
local resume = Bitmap.new(self.g:getTextureRegion("resume.png"))
resume:setAnchorPoint(0.5, 0.5)
local resumeButton = Button.new(resume)
resumeButton:setPosition(conf.width/2, 100)
resumeButton:addEventListener("click", self.closeMenu, self)
menu:addChild(resumeButton)
Тепер ви можете знову запустити проект і спробувати відкрити та закрити меню з допомогою кнопок menu  та  resume.

Використання фізики в Gideros

  Зараз наступає найвеселіша частина. Давайте встановимо все, що нам потрібно для використання фізичного двигуна, який буде симулювати нам сили гравітації та зіткнення. Фізичний движок Gideros називається Box2D.

По-перше, щоб вказати Gideros , що ми хочемо використовувати фізичний двигун, нам потрібно його підключити- require . Напишіть  require "box2d" на початку файлу main.lua.

Потім нам потрібно змінити файл LevelScene.lua. По-перше, в межах методу LevelScene: init, ми повинні створити новий фізичний світ, world вказавши, який тип гравітації ми хочемо використати в ньому.

Гравітація визначається як вектор, який представляє напрямок прискорення. Цей вектор визначається  двума параметрами: x і y. Змінна x - гравітація на осі x; якщо значення є позитивним, об'єкти падають на правий бік, а якщо значення є негативним, об'єкти падають на ліву сторону. Те саме правило застосовується до змінної y на осі y.
Якщо ви надасте рівні значення x та y, наприклад, 10, то об'єкти будуть падати в нижньому правому куті. Чим вище значення, тим більшим є прискорення. Нульове значення -створить  невагомість, це підійде для таких ігор, як більярд. Ми будемо використовувати гравітацію  землі  або стандартне прискорення через вільне падіння, яке становить 9,8м /с2

Вкажемо  третій параметр як true,, що вказує на те, що ми дозволили фізичним тілам спати, і це є більш ефективним способом використання фізичного двигуна. В основному, в більшості випадків  треба дозволяти фізичним тілам спати.

Після створення світу world  ми збережемо його як властивість екземпляра, щоб ми могли отримати доступ до нього за допомогою будь-якого методу.

--створити екземпляр world 
self.world = b2.World.new(0, 9.8, true)
Далі ми налаштуємо відображення  фізичних тіл об'єктів для відладки, щоб ми могли бачити, як фізичні об'єкти фактично рухаються, і, отже, правити, якщо це щось нетак, ніж як ми хочемо.

Для цього, в   init, ми створимо шар DebugDraw і додамо його до нашої сцени.

--намалювати фізичне тіло для дебагу
local debugDraw = b2.DebugDraw.new()
self.world:setDebugDraw(debugDraw)
self:addChild(debugDraw)

Створення фізичних тіл

Тепер, коли це налаштовано, давайте створимо наш перший фізичний об'єкт і спробуємо побачити, як воно рухається.
Створимо нову папку objects у нашому проекті в Gideros Studio, там ми розмістимо всі файли, пов'язані з нашими об'єктами гри.

Потім створіть файл MainBall.lua всередині цього каталогу і створіть у ньому клас MainBall, який успадковує від Sprite.

MainBall = Core.class(Sprite)
Потім давайте додамо метод init нашому класу MainBall. Він повинен приймати три аргументи, level -посилання на екземпляр сцені, а також х і у, координати ,  розміщення на сцені. Ми збережемо посилання на сцену як властивості self.level.

function MainBall:init(level, x, y)
 self.level = level
 self:setPosition(x,y)
end
Тепер всередині MainBall: init, давайте додамо зображення, яке представлятиме наш основний м'яч. Ми будемо створювати об'єкт Bitmap з нашої упакованої текстури та встановити точку прив'язки до значення 0,5. Встановлення 0.5, значення опорної точки є обов'язковим, оскільки фізичні тіла також посилаються на центр м'яча, і нам потрібно  привести координати (фізичне тіло, так і наш об'єкт MainBall) до відповідності.

Після цього ми просто додамо цей Bitmap  зображення до об'єкта MainBall.

--створити bitmap обєкт  зоображення м'яча
self.bitmap = Bitmap.new(self.level.g:getTextureRegion("main1.png"))
--перемістити точку привязки в центр зоображення м'яча
self.bitmap:setAnchorPoint(0.5,0.5)
self:addChild(self.bitmap)
Тепер нам потрібно створити фізичне тіло  body такого ж розміру і форми  як зоображення. Спочатку давайте розрахуємо радіус м'яча, взявши ширину зображення і поділивши його на 2.

--отримати радіус
local radius = self.bitmap:getWidth()/2
Потім ми можемо створити об'єкт body, командою world:createBody та вказавши тип тіла. Є три можливі типи тіл: статичні static,, кінематичні kinematic, та динамічні dynamic. .

Статичне тіло static не рухається за правилами фізики, визначеними у вашому світі, він діє так, ніби він має нескінченну вагу (її не можна перемістити іншими об'єктами, що стикаються з нею), її можна переміщувати лише шляхом прямого встановлення його положення в коді.

Кінематичне тіло kinematic схоже на статичне тіло, воно також має нескінченну вагу і не дотримується правил фізики, але на відміну від статичного тіла воно має швидкість. Ви можете визначити швидкість, з якою вона буде рухатися, і його  траєкторія не буде змінена через зіткнення з іншими тілами.

Динамічне тіло dynamic повністю дотримується всіх законів фізики, змінює траєкторію при зіткненнях тощо. Ви також можете застосувати різні сили до динамічних об'єктів. Це саме те, що ми хочемо від цього об'єкта, тому ми надамо значення b2.DYNAMIC_BODY, яке вказує на те, що наше тіло буде динамічним

--створіть фізичний об'єкт box2d
local body = self.level.world:createBody{type = b2.DYNAMIC_BODY}
Фізичні тіла не відносяться до ієрархії видимих обєктів. У них є власний світ, в якому вони працюють, тому перше, що потрібно зробити, - скопіювати поточний статус об'єкта MainBall в фізичне тіло. Ми копіюємо позицію та кут, в якій вона знаходиться в даний час. Зверніть увагу, що Gideros використовує кути в градусах, але box2D використовує радіани;
таким чином ми повинні перетворити наші кути з градусів в радіани.

body:setPosition(self:getPosition())
body:setAngle(math.rad(self:getRotation()))
Після цього нам потрібно створити форму тіла. Існують різні її варіанти, такі як PolygonShape багатокутники або ChainShape мотузка. Але в цій ситуації ми створюємо кулю, тому нам потрібен CircleShape коло . Ми створюємо його, надаючи йому координати центру, які будуть (0,0) центром форми і радіуса кола.

local circle = b2.CircleShape.new(0, 0, radius)
Коли ми визначимо форму, ми створимо каркас тіла  з цієї форми та іншими фізичними властивостями, такими як:


  • Density: Щільність, визначає масу предмета, розраховану на основі розмірів та щільності
  • Friction: вона визначає тертя тіла іншими поверхнями
  • Restitution:: він визначає пригучість тіла

Якщо Restitution < 1, це означає, що якщо ви скидаєте м'яч, він відскочить назад, але відстань кожної відмов буде меншою, ніж попередній відплив, і відстань буде зменшуватися, поки об'єкт не зупиниться. Якщо Restitution = 1, цей м'яч відскочить прямо назад до тієї ж позиції, знову падатиме і знову відновлять вічно.
Це не втратить дистанції або швидкості, а якщо Restitution >1, швидкість і відстань збільшаться.

Значення, які я надаю, подібні до того, що було використано в Mashballs, але ви можете експериментувати з ними для досягнення будь-якого бажаного ефекту.

local fixture = body:createFixture{shape = circle, density = 1.0, friction = 0.1, restitution = 0.5}
І останнє, ми зберігаємо тип об'єкта type , як властивість екземпляра нашого класу MainBall, а також зберігаємо посилання на цей клас на нашому тілі . Таким чином, ми завжди можемо отримати властивості один від одного, незалежно від того, який світ (Gideros або Box2D) ми знаходимося.

body.type = "main"
self.body = body
body.object = self
Тепер давайте змінимо наш клас MainBall. Давайте перейдемо до методу LevelScene: init () і створіть екземпляр класу MainBall шляхом передачі посилання на екземпляр LevelScene розмістіть його  в координатах (100, 100) і збережіть його як властивість екземпляра LevelScene.

Не забудьте також додати його на сцену, щоб він відобразився на екрані.

self.mainBall = MainBall.new(self, 400, 200)
self:addChild(self.mainBall)
Коли це зроблено, спробуйте запустити проект і натисніть кнопку "Start Game", щоб перейти до LevelScene. Ми повинні побачити графіку нашого  м'яча та прозорого червоного круга навколо нього, фігура тіла для дебагу.

Як ви бачите, фігура дебагу трохи більша за нашу графіку, і це тому, що ми вказали  половину ширини графіки як радіус тіла. Але наша графіка не може бути ідеальним колом (у нашому випадку воно має волосся). Вона також може містити прозорі ділянки навколо нього.

Це є насправді однією з причин використання фігури для відладки . Тепер ми можемо налаштувати розмір тіла відповідно до нашого зображення.


Тепер давайте перейдемо до  MainBall: init (), де ми визначали радіус і зробимо його трохи менше, множивши на 0.85.

--отримати радіус
local radius = (self.bitmap:getWidth()/2)*0.85
Якщо ви повторно запустите проект, ви повинні побачити, що фігура дебагу навколо зображення, стала по розміру смайлика.


Запуск світу world

Ви повинні були помітити, що обєкт і фізичне тіло відображаються,  але вони не рухаються.

Це тому, що ми також маємо керувати нашим світом world крок за кроком, буквально. Нам потрібно періодично викликати world:step () метод для оновлення стану світу фізики.

Для цього нам потрібно визначити обробник події, який буде викликати подію ENTER_FRAME. Подія ENTER_FRAME виконується кожного разу, коли екран(фрейм) оновлюється . Таким чином, вся анімація, переміщення речей і т. Д. Виконуються всередині цієї події. Запуск фізичного світу не є винятком.

Тож давайте визначимо простий метод LevelScene:onEnterFrame для обробки події ENTER_FRAME, в якому ми будемо оновлювати стан нашого world . У цій події , ми спочатку перевіряємо, чи наша гра на не паузі paused , а потім викликаємо  метод self.world:step (), вказавши кількість кроків, які ми повинні виконати у світі.

function LevelScene:onEnterFrame()
 if not self.paused then
 -- оновити world 
 self.world:step(1/60, 8, 3)
 end
end
Тепер давайте додамо слухача цієї події в метод LevelScene: init ():
-запустити world
self:addEventListener(Event.ENTER_FRAME, self.onEnterFrame, self)
Якщо ми зараз запустимо світ, ми побачимо, що наша дебагова фігура рухається, але наше зображення Bitmap не слідує. Це тому, що нам також потрібно оновити позиції об'єкта Sprite на основі їхніх позицій тіла.

Це означає, що нам також знадобиться місце, де зберігатимуться всі об'єкти Sprite, які матимуть фізичні тіла, для цього створимо порожню таблицю self.bodies в методі LevelScene: init ().

--зберігальня тіл bodies  та спрайтів  тут
self.bodies = {}
Тепер давайте відредагуємо подію onEnterFrame, щоб ми пройшли всі тіла і відповідно оновили позиції своїх об'єктів.

Для кожного тіла ми будемо оновлювати позицію об'єкта та кут його візуального зображення на екрані. Зверніть увагу, що тут ми знову змінюємо кут з радіана до градус через відмінності між Gideros і Box2D.

Зауважте, ми визначили local body  змінну тіла поза циклом. Це лише для економії, нам не треба оновлювати цю змінну та виділити для неї  пам'ять на кожному кроці цикла.

function LevelScene:onEnterFrame()
 if not self.paused then
 -- оновити стан світу world 
 self.world:step(1/60, 8, 3)
 --повторити  для всіх дочірніх спрайтів 
 local body
 for i = 1, #self.bodies do
 --отримати тіло  body
 body = self.bodies[i]
 --оновити позицію об'єкта, щоб відповідати світовому об'єкту box2d
 position
 --застосувати координати до спрайт
 body.object:setPosition(body:getPosition())
 --застосувати обертання до спрайту
 body.object:setRotation(math.deg(body:getAngle()))
 end
 end
end
Далі повернімося до MainBall.lua і додамо body до таблиці bodies сцени у методі init. Це важливо, тому що, не додаючи тіла до таблиці, його позиція не буде оновлюватися всередині дії LevelScene: onEnterFrame.

table.insert(self.level.bodies, body)
Тепер спробуйте запустити проект, і ви побачите, що наше зображення буде рухатися відповідно до дебагової  фігури .

Налаштування  кордонів world 

Все, здається, добре працює. Але, привіт! М'яч просто падає з екрану. Ось чому ми повинні встановити world кордони.

Оскільки це не буде об'єктом, з яким ми будемо взаємодіяти багато в грі, а буде просто  рамка, ми не будемо створювати для нього окремий клас, а скоріше просто використаємChainShape і створити простий прямокутник, який оточує наш екран.

Ми не потребуємо жодного графічного зображення для його відображення (у нас вже є кордони, намальовані у нашому фоновому малюнку), і нам не доведеться оновлювати свою позицію, бо це буде статичне тіло.

Так що давайте створимо його в методі LevelScene: init (). Як і раніше, ми створюємо body, але тепер надаємо константу b2.STATIC_BODY, що вказує на те, що ми створюємо статичне тіло, і ми встановлюємо позицію цього body в координатах (0,0).

local body = self.world:createBody{type = b2.STATIC_BODY}
body:setPosition(0, 0)
Потім ми створюємо об'єкт ChainShape і надаємо йому екранні розміри. Цей процес схожий на малювання за допомогою класу Shape.

local chain = b2.ChainShape.new()
chain:createLoop(
 0,0,
 conf.width, 0,
 conf.width, conf.height,
 0, conf.height
)
Нарешті, ми створюємо fixture зі створеними shape та деякими фізичними властивостями.

local fixture = body:createFixture{shape = chain, density = 1.0, friction = 1, restitution = 0.5}
Тепер після запуску нашого проекту та переходу на сцену level  , ви побачите, що основний м'яч падає вниз екрану і залишається там.

Взаємодія з фізичними тілами

Тепер наш м'яч просто падає на землю, але ми хочемо запустити його з рогатки, як у реальній грі Машбол.

Взаємодючи з об'єктами Sprite ми просто змінюємо їхні координати і так далі, але ми не можемо їх зробити з фізичними тілами, особливо не з динамічними, оскільки ми тільки рухатимемо образ, але фізичне тіло все одно буде залишити у попередній позиції.

Тому ми повинні  фізично взаємодіяти з динамічними об'єктами, застосовуючи сили force , імпульси impulses, крутний момент torque,, і шляхом встановлення швидкості velocities фізичних тіл. Настройка швидкості velocities це рух тіла в заданому напрямку, поки воно не буде протистояти  з іншими силами (як у випадку зіткнень). Налаштування крутного моменту torque  в основному означає, що тіло обертається навколо свого центру.

Поки легко пояснити швидкість velocities  і крутний момент torque, різниця між силою force  та імпульсом impulses не настільки велика, навіть непомітна. Вони обидва застосовують силу імпульсу до об'єкта, залишаючи лише інерцію, безперервного руху, як у випадку velocities .

Але що робить  force  і  impulses різними?  :

  force  -це постійна сила наприклад вітер який рухає м'яч, impulses -це тимчасова сила -наприклад буцнути м'яч ногою задавши йому імпульс прискорення

  1. імпульс impulses, застосовується миттєво, тоді як сила force почне працювати тільки після world:step  буде викликаний на наступний раз.
  2. По-друге, impulses необмежений часом і застосовується постійно, тоді як сила force   застосовується лише на частку секунди (між двома world:step викликами метода). А щоб досягти такогож ефекту , що і при impulses , вам потрібно буде повторно застосувати його на кожний world:step на цілу секунду.
  3.  оскільки force застосовується лише невеликими фрагментами часу, на нього більше впливають інші сили, такі як сила тяжіння gravity. . 


Тому в більшості випадків, коли ви хочете запустити об'єкт у певному напрямку,
ви будете використовувати імпульс impulses, і якщо ви хочете застосувати певну силу протягом певного періоду часу, ви будете використовувати силу force  .

Отже, в оригінальній грі, спочатку є лише bitmap зоображення, , користувачі вдтягують його прицілюючись. Після того як користувач випускає його, нам потрібно створити фізичне тіло body  для нашого bitmap   та застосувати імпульс impulses, в напрямку позиції спрайту до перетягування  .

Почнемо з відділення процесу створення тіла з методу MainBall: init () до нового методу під назвою MainBall: createBody (),, який мивикористаємо  пізніше, щоб створити body для нашого зображення


function MainBall:createBody()
 --визначити радус
 local radius = (self.bitmap:getWidth()/2)*0.85
 --створити box2d фзичний об'єкт
 local body = self.level.world:createBody{type = b2.DYNAMIC_BODY}
 --копіювати координати зоображення в фізичне тіло
 body:setPosition(self:getPosition())
 body:setAngle(math.rad(self:getRotation()))
 local circle = b2.CircleShape.new(0, 0, radius)
 local fixture = body:createFixture{shape = circle, density =
 1.0,
 friction = 0.1, restitution = 0.2}
 body.type = "main"
 self.body = body
 body.object = self
 table.insert(self.level.bodies, body)
end
Тепер давайте створимо метод onMouseDown - обробку події для випадку MOUSE_DOWN, коли користувач починає торкатися смайла.

Всередині цього нам не потрібно перевіряти hitTestPoint, оскільки ми хочемо, щоб пристрій дотику працював на весь екран. Але що нам потрібно зробити, це зупинити поширення propagation на інші події, зберегти наші нинішні координати як початкову позицію і встановити прапор, що наш головний м'яч-смайл зараз тягнеться dragged. .

function MainBall:onMouseDown(e)
 e:stopPropagation()
 self.startX = self:getX()
 self.startY = self:getY()
 self.isDragged = true
end
Наступним кроком для обробки є подія MOUSE_MOVE, тому створіть метод onMouseMove.

Спочатку ми перевіряємо, чи тягнеться  наш основний м'яч, перевіряючи прапорець isDragged, і якщо його тягнуть, давайте зупинимо подальше поширення події. Тоді ми хочемо обмежити радіус перетягування нашого основного м'яча-смайла, таким чином давайте встановимо радіус r до 120, щоб обмежити перетягування до 120 пікселів.

Ми обчислимо поточні вектори xVect і yVect на основі початкових та поточних позицій. Потім ми обчислимо відстань від початкової позиції на основі розрахованих векторів і зберігаємо їх у змінній length. .

Після цього ми можемо просто перевірити, чи length  менша або дорівнює дозволеному радіусу, ми просто змінюємо положення нашого об'єкта на координати події миші.

Якщо length   довша, ніж дозволений радіус, нам потрібно розрахувати коефіцієнт, за допомогою якого ми можемо помножити вектор, щоб отримати вектор, який буде відповідати нашим дозволеним радіусом. Іншими словами, ми встановлюємо об'єкт на граничні координати нашого радіусу, враховуючи наданий користувачем напрямок.

function MainBall:onMouseMove(e)
 if self.isDragged then
 e:stopPropagation()
 local r = 120
 local xVect = (e.x-self.startX)
 local yVect = (e.y-self.startY)
 local length = math.sqrt(xVect*xVect + yVect*yVect)
 if length <= r then
 self:setPosition(e.x, e.y)
 else
 local coef = math.sqrt((r*r)/(xVect*xVect+yVect*yVect))
 self:setX(self.startX+xVect*coef)
 self:setY(self.startY+yVect*coef)
 end
 end
end
Останнє, що потрібно зробити, полягає в обробці події MOUSE_UP за допомогою методу onMouseUp

Всередині ми аналогічно перевіряємо, чи наш головний м'яч тягнеться, і припиняємо поширення подій, якщо це є. Тоді ми знімаємо прапор нашого об'єкта, який тягнеться, а в  тому, що подія MOUSE_UP означає, що наш об'єкт був відпущений.

Це також означає, що тепер ми можемо створити фізичне тіло для нашого об'єкта, викликавши наш раніше створений метод MainBall: createBody ().

Потім ми повинні застосувати імпульс на основі напрямку та відстані від початкової точки. Ми визначаємо змінну маси тіла strength зі значенням 10; ми будемо використовувати це для збільшення інерції(ви можете змінити її, якщо хочете запустити об'єкти з різними відносними силами).

Тоді ми знову обчислюємо вектор з початкової до  поточної позиції, помножений на силу, яку ми щойно визначили, і ми застосовуємо імпульс до цих векторів, викликаючи метод self.body: applyLinearImpulse  і надаючи вектори та координати як центр мас (або просто поточні координати об'єкта ).

Останнє, що потрібно зробити, полягає в тому, щоб видалити всіх слухачів подій миші з нашого об'єкта, оскільки ми більше не будемо їх використовувати, доки не буде завантажена наступна сцена, коли ми створимо новий смайл-м'яч.

function MainBall:onMouseUp(e)
 if self.isDragged then
 e:stopPropagation()
 self.isDragged = false
 self:createBody()
 --визначити силу рогатки
 local strength = 10
 --розрахувати вектор сили на основі сили strength
 --і відстань тяги
 local xVect = (self.startX - self:getX())*strength
 local yVect = (self.startY - self:getY())*strength
 --застосувати імпульс до м'яча
 self.body:applyLinearImpulse(xVect, yVect, self:getX(), self:getY())
 self:removeEventListener(Event.MOUSE_DOWN, self.onMouseDown, self)
 self:removeEventListener(Event.MOUSE_MOVE, self.onMouseMove, self)
 self:removeEventListener(Event.MOUSE_UP, self.onMouseUp, self)
 end
end
Тепер запустіть проект, перейдіть на сцену свого рівня та спробуйте потягнути і відпустити смайл-м'яч. Це має поводитися так само, як ви бачили в реальній грі.

Обробка колізій Box2D


Тепер створіть інший тип ігрового елемента, який називається TouchBall , який буде представляти собою м'яч, в який основний м'яч -смайл повинен потрапити, і ми дізнаємося про це, прослуховуючи події зіткнення collision.

Тому спочатку створіть файл TouchBall.lua всередині папки об'єктів та визначте в ньому клас TouchBall, який успадковує від Sprite.


TouchBall = Core.class(Sprite)
Створення об'єкта TouchBall буде дуже схожим на створення MainBall. Він буде приймати ті самі параметри і робити абсолютно те саме , лише завантажуючи іншу графіку та встановлюючи інший тип тіла.

Тому спочатку ми зберігаємо посилання на сцену та встановлюємо положення об'єкта. Потім ми створюємо об'єкт Bitmap з графіки сенсорного кульки та встановлює його точку прив'язки до 0,5 і додамо цей об'єкт Bitmap до екземпляру класу TouchBall.

function TouchBall:init(level, x, y)
 self.level = level
 self:setPosition(x,y)
 --створення bitmap об'єкта з м'ячем 
 self.bitmap =
 Bitmap.new(self.level.g:getTextureRegion("touch4.png"))
 --встановлення точки прив'язки
 self.bitmap:setAnchorPoint(0.5,0.5)
 self:addChild(self.bitmap)
end
Потім так само, як і в класі MainBall, ми можемо визначити метод TouchBall: createBody (). Ми отримуємо радіус від половини ширини зображення і помножте його на корекцію 0.85. Потім ми створюємо тіло, тільки зараз, передаючи значення b2.STATIC_BODY для створення статичного тіла. Після цього ми копіюємо положення та кут представленого растрового зображення.
Тепер нам потрібно створити форму кола з наданим радіусом і створити fixture, використовуючи цю форму. І так само, як і в MainBall, ми встановлюємо посилання на тіло і об'єкт, щоб ми могли використовувати їх в обох world. .

function TouchBall:createBody()
 --отримати радіус
 local radius = (self.bitmap:getWidth()/2)*0.85
 --створити box2d фізичний об'єкт
 local body = self.level.world:createBody{type = b2.STATIC_BODY}
 --копіювати  стан об'єкта
 body:setPosition(self:getPosition())
 body:setAngle(math.rad(self:getRotation()))
 local circle = b2.CircleShape.new(0, 0, radius)
 local fixture = body:createFixture{shape = circle, density = 1,
 friction = 0.1, restitution = 0.5}
 body.type = "touch"
 self.body = body
 body.object = self
end

Нам не потрібно додавати тіло TouchBall до списку тіл  table. insert(self.level.bodies, body) . Вставити (self.level.bodies, body), оскільки це статичне тіло, і воно не рухається. Але якщо ми створили динамічний об'єкт, нам також доведеться включити його в список тіл

Отже, давайте перейдемо до методу LevelScene: init () і додамо цей TouchBall на сцену нашого рівня. Ми робимо це аналогічним чином, створюючи екземпляр класу TouchBall, забезпечуючи всі необхідні параметри та додаючи цей екземпляр на сцену.

local touch = TouchBall.new(self, 200, 100)
self:addChild(touch)
Тепер, якщо ви запустите проект, ви побачите, що ми  маємо об'єкт TouchBall, який з'являється на нашій сцені. Оскільки ми маємо два різних об'єкти, давайте послухаємо, коли вони зіткнуться і змінять зображення головного м'яча на смайл.

Спочатку налаштуємо метод MainBall: smile () у файлі MainBall.lua. Для цього ми просто змінить текстуру растрового зображення MainBall на іншу, з смайлом, а потім змінить його назад через 2 секунди (або 2000 мілісекунд) за допомогою функції Timer.delayedCall,
який виконає надану функцію після заданої кількості мілісекунд.


function MainBall:smile()
 local smileTexture = self.level.g:getTextureRegion("main2.png")
 self.bitmap:setTextureRegion(smileTexture)
 Timer.delayedCall(2000, function()
 local normal = self.level.g:getTextureRegion("main1.png")
 self.bitmap:setTextureRegion(normal)
 end)
end
Тоді давайте перейдемо до LevelScene.lua і створіть новий метод обробки зіткнень, який називається методом LevelScene: onBeginContact (). Там ми отримаємо fixtures, які стикаються з об'єктом події. Тоді ми можемо отримати тіла fixtures. Після цього нам потрібно визначити, які тіла стикаються.

Спочатку ми перевіряємо, чи обидва тіла мають певний тип властивостей, як ми визначили їх як у класах MainBall, так і в TouchBall. Якщо вони не визначені для обох тіл, головний м'яч, швидше за все, зіткнеться з прикордонною стіною, і ми не зацікавлені в цьому зіткненні. Але якщо ми визначили типи для обох органів, це зіткнення має нас зацікавити.

Після цього ми повинні визначити, які тіла насправді стикаються, який з них є основним, який є зтикаємим тощо. Зазвичай вам доведеться обходитись і перевіряти кожен можливий варіант, наприклад перевірити,   bodyA, а потім перевірити,  bodyB, і так далі.

if bodyA.type == "main" then
 local main = bodyA
elseif bodyB.type == "main"
 local main = bodyB
end
Але ось акуратний трюк. Box2D розміщує перше створене тіло як bodyA і друге створене тіло bodyB. Отже, якщо зіткнеться два тіла, перше, що було створено, буде bodyA.

Тепер, якщо ви пам'ятаєте, ми створюємо тіло для основного м'яча лише після того, як ми торкнемося, перетягнемо його та випустімо. Це означає, що всі елементи на сцені повинні завантажуватися раніше. Отже, ми маємо гарантію, що, якщо основний м'яч стикається з будь-яким об'єктом, це завжди буде  bodyB.

Коли ми визначимо, чи є зіткнення між 1 та 2 кулями, ми можемо викликати метод smile () на властивість bodyB об'єкта, який, як ми пам'ятаємо, є посиланням на клас MainBall.

function LevelScene:onBeginContact(e)
 --отримання контактних тіл
 local fixtureA = e.fixtureA
 local fixtureB = e.fixtureB
 local bodyA = fixtureA:getBody()
 local bodyB = fixtureB:getBody()
 --перевірте, чи ця коллізія нас цікавить
 if bodyA.type and bodyB.type then
 --перевірте, чи ця коллізія нас цікавить
 if bodyA.type == "touch" and bodyB.type == "main" then
 --smile
 bodyB.object:smile()
 end
 end
end
Тепер перейдіть на сцену нашого рівня і спробуйте натиснути на м'яч дотиком з головним м'ячем, і ви побачите, що головний м'яч посміхатиметься на 2 секунди після удару по м'ячу, що означає, що ми успішно слухали подію зіткнення і визначили, які тіла були насправді стикаються
Щоб зіткнутись тільки перший хіт,ми можемо видалити тип колізії  з м'ячем при зіткненні, щоб знати, що ми вже потрапили в нього, і ми більше не зацікавлені цим об'єктом, і основний м'яч посміхається лише один раз за зіткнення з кожним торканням м'яча.

--смайл
bodyB.object:smile()
bodyA.type = nil

Управління пакетами та рівнями

Тепер, коли ми закінчили прототип нашої гри і бачимо, що це працює, давайте реалізовувати логіку пакету та рівня, а також прогрес між гравцями між рівнями.
Це частина, в якій ви виймаєте свою гру з прототипу і зробите її більш циклами взаємодії та почуттям прогресії, і внесіть її у те, що ви можете показати іншим.
Зазвичай я починаю турбуватися про те, як мої друзі пробують нову гру після реалізації простого пакета уроків.

Визначення пакетів


Спочатку створіть простий файл, де ми визначаємо наші пакети та кількість рівнів у них. В Gideros додайте до вашого проекту файл packs.lua. Всередині ми визначимо таблицю простих пакетів, яка міститиме субтаблиці, що представлятимуть кожний пакет з ім'ям пакету та кількість рівнів у ній.

packs = {
 {
 name = "Перший пакет",
 levels = 15
 },
 {
 name = "Другий пакет",
 levels = 15
 },
}
На основі цього визначення ми можемо створити сцену, де ми можемо дозволити користувачеві переключатися між пакетами та вибрати будь-який рівень у пакеті.


Створення LevelSelectScene

Створіть LevelSelectScene.lua в папці scenes проекту Gideros і визначте клас LevelSelectScene, успадкований від Sprite в ньому.


LevelSelectScene = Core.class(Sprite)

Тепер давайте створимо метод init і покладемо в нього звичайні речі, фонову графіку, кнопку зворотного зв'язку, а також код для обробки кнопки back Android, який в обох випадках повернеться на start сцену.

function LevelSelectScene:init()
local bg = Bitmap.new(Texture.new("images/bg.jpg", true))
 bg:setAnchorPoint(0.5, 0.5)
 bg:setPosition(conf.width/2, conf.height/2)
 self:addChild(bg)
 local backText = TextField.new(conf.fontMedium, "Back")
 backText:setTextColor(0xffff00)
 local backButton = Button.new(backText)
 backButton:setPosition((conf.width - backButton:getWidth())/2,
 conf.height - 30)
 self:addChild(backButton)
 backButton:addEventListener("click", function()
 sceneManager:changeScene("start", conf.transitionTime,
 conf.transition, conf.easing)
 end)
 self:addEventListener(Event.KEY_DOWN, function(event)
 if event.keyCode == KeyCode.BACK then
 sceneManager:changeScene("start", conf.transitionTime,
 conf.transition, conf.easing)
 end
 end)
end
Як завжди, ми також повинні додати цю сцену до нашого sceneManager у файлі main.lua.

--level select scene
["levelselect"] = LevelSelectScene,
Тепер, коли у нас є сцена levelSelect між StartScene та LevelScene, нам потрібно правильно їх поєднати бо, натискаючи кнопку Start Game, ми будемо йти на сцену levelselect, а не на сцену level.
Отже, давайте подивимось на нашу StartScene.lua і змінюємо подію в startButton  перехід на сцену levelselect.

startButton:addEventListener("click", function() sceneManager:changeScene("levelselect", conf.transitionTime, conf.transition, conf.easing)end)

Якщо ви запустите проект зараз і натиснете кнопку «Start Game», ви повинні перейти на сцену levelselect, яка матиме кнопку «Back»; натиснувши на неї, ви повернетесь до StartScene.

Наступним завданням є дозволити користувачеві вибрати level з певного пакета pack.. Таким чином, ми повинні знати, який поточний вибраний pack. Було б добре зберігати останній вибраний pack  , отже, коли користувач повернеться до гри, він / вона побачить останній пакет pack, що відобразиться на екрані вибору level .
У нас є клас Settings для цієї мети.

Отже, відкрийте свої Settings.lua всередині папки класів і додайте пару нових значень до своєї початкової таблиці налаштувань, curPack для поточного пакету та curLevel для вибраного рівня, який нам також буде потрібен пізніше.

--our initial settings
local settings = {
 username = "Player",
 curPack = 1,
 curLevel = 1
}
Тепер, коли гравець відкриває гру, пакет pack  (і рівень level ) за замовчуванням буде обраний як 1. Так що давайте повернемося до нашого LevelSelectScene.lua і вдобразимо для вибору декілька рівнів
Як ви пам'ятаєте, ми визначили інформацію про наші pack   всередині packs.lua. Тож давайте використаємо його та покажемо ім'я поточного вибраного пакета як заголовок.
Спочатку нам потрібно отримати поточний pack  з Settings  і встановити його як властивість екземпляра класу LevelSelectScene.

self.curPack = sets:get("curPack")
Тоді ми можемо створити TextField з нашим визначеним шрифтом середнього рівня та надавати назву пакета як текст. Потрібно лише встановити колір тексту, вказати бажану позицію та додати його до сцени.

local packHeading = TextField.new(conf.fontMedium,
 packs[self.curPack].name)
packHeading:setTextColor(0xffff00)
packHeading:setPosition((conf.width - packHeading:getWidth())/2,
 50)
self:addChild(packHeading)

Генерування екрану вибору рівнів

Настав час створити екран вибору  рівня, де наш гравець може бачити, скільки рівнів у пакеті, а також статус рівнів, заблокований і розблокований.
Ви можете побачити приклад отриманого екрана на наступному скріншоті:


Тепер давайте копіювати зображення, які ми будемо використовувати для заблокованих / розблокованих рівнів до папки зображень нашого проекту, і додамо їх до Gideros Studio.
Потім створіть Sprite-слой для нашої сітки та додайте його на сцену.

Синтаксис: Lua
local grid = Sprite.new()
self:addChild(grid)

Після цього нам потрібно визначити  змінну, таку як поточна позиція, де ми будемо розміщувати значок наступного рівня, крок збільшення позиції з кожним значком, відстань між значками, кількість загальних стовпців у рядку та поточний ряд ми обробляємо.

local currentX, currentY = 0, 0 -- стартові координати
local step = 100 -- збільшити на рівень
local padding = 20 --підкладка між стовпцями та рядками
local totalCol = 5 -- загальна кількість стовпців
local curCol = 0 --поточний  стопчик
Далі ми повинні визначити цикл, щоб перейти від рівня 1 до кількості рівнів, визначених у packs.lua.

for i = 1, packs[self.curPack].levels do
end
У цьому циклі нам потрібно створити зображення, яке відображатиме значок рівня. Наразі ми покажемо всі рівні як заблоковані. Ми створюємо об'єкт Bitmap з текстури зображення і встановлюємо його положення на currentX і currentY позиції та додаємо його до grid

--створити зображення рівня
local level = Bitmap.new(Texture.new("images/level_locked.png", true))
level:setPosition(currentX, currentY)
grid:addChild(level)
Далі створіть levelNumber для відображення у верхньому лівому куті значка рівня. Ми можемо взяти номер поточного рівня з змінної циклу "i" та  створити об'єкт TextField зі своїм значенням. Потім ми встановлюємо його колір та положення у верхньому лівому куті та додаємо його до значка рівня.

--додати номер рівня
local levelNumber = TextField.new(conf.fontMedium, i)
levelNumber:setTextColor(0xffffff)
levelNumber:setPosition(10, 40)
level:addChild(levelNumber)
Як останнє, ми повинні обчислити правильну позицію, де розташувати значок наступного рівня. Таким чином, ми збільшуємо лічильник curCol і перевіряємо, чи досягло максимального значення totalCol у рядку. Якщо так, ми скидаємо позиції curCol і currentX і збільшуємо currentY позицію, щоб перейти до наступного рядка, а якщо ні ми просто збільшуємо currentX позицію, щоб перейти до значка наступного рівня в рядку.

Від перекладача: є набагато кращий варіант ніж в цій книзі  зробити цикл в циклі,  один цикл проходитиме по стовпцях, а всередині нього цикл проходитиме по рядках
--маніпулювати рівнем положення в сітці
curCol = curCol + 1
if curCol == totalCol then
 curCol = 0
 currentX = 0
 currentY = currentY + padding + step
else
 currentX = currentX + padding + step
end
Як результат, у нас є сітка рівнів для вибору з нашого поточного пакету.

Переключення між пакетами

Але як ми можемо змінити пакет? Додамо зображення правої та лівої кнопки, щоб вибрати пакети. Спочатку скопіюйте зображення до папки зображень вашого проекту та додайте їх у програму Gideros Studio у папці images.

Отже, як ми могли б фактично переключити пакет на тій самій сцені? Ну, оскільки ми отримали наш поточний пакет від налаштувань, це малоб сенс, якщо б ми просто змінили параметри curPack всередині settings, і повернулися прямо до цієї ж сцени. Дозвольте мені показати вам, як це зробити.


Спочатку давайте визначимо метод LevelSelectScene: nextPack (). Всередині цього ми перевіримо, чи є сам.curPack не останньою pack.. Ми можемо це зробити, перевіривши кількість пакетів у таблиці пакетів, і якщо вона більше, ніж кількість поточного пакета, ми заощаджуємо self.curPack + 1 в класі Settings і скидаємо цю сцену.
Таким чином, наступного разу, коли ця сцена буде завантажена, вона буде отримувати  встановлений номер self.curPack та  завантажвати його назву та рівні.

Ще однією річчю слід зазначити, що для більш цікавого переходу ми вибрали перехід SceneManager.moveFromRight, тому що ми хочемо, щоб новий пакет прийшов з правого боку.


function LevelSelectScene:nextPack()
 if self.curPack < #packs then
 sets:set("curPack", self.curPack + 1)
 sceneManager:changeScene("levelselect", conf.transitionTime,
 SceneManager.moveFromRight, conf.easing)
 end
end
Давайте створимо дуже схожий метод LevelSelectScene: prevPack () для переходу до попереднього pack.. Аналогічним чином ми перевіряємо, чи self.curPack не є першим пакунком, і якщо це не так, ми зменшуємо поточний пакет на один і зберігаємо його в налаштуваннях і знову скидаємо цю сцену.


function LevelSelectScene:prevPack()
 if self.curPack > 1 then
 sets:set("curPack", self.curPack - 1)
 sceneManager:changeScene("levelselect", conf.transitionTime,
 SceneManager.moveFromLeft, conf.easing)
 end
end
Оскільки ми використовували комбінацію сценічних переходів як SceneManager.moveFromRight для наступного пакета та SceneManager.moveFromLeft для попереднього пакета, ми матимемо хороший ефект для перемикання між пакетами.

Тепер давайте створимо кнопки, щоб спробувати переключити пакет. Перейдіть до методу LevelSelectScene: init (), і давайте додамо кнопку, щоб перейти на наступний пакет. Знову ж таки, нам не потрібна ця кнопка, якщо вона є останньою сценою, тому давайте перевіримо, чи це не остання сцена, а потім просто створіть об'єкти Bitmap з малюнком стрілка вправо, та стрілка вліво і створимо кнопку з цих об'єктів Bitmap


Далі ми встановлюємо позицію з змінними зміщення conf.dx та conf.dy, щоб зробити його прикріпленим до нижнього правого кута. Потім ми просто додаємо його до сцени та додаємо метод nextPack () як обробник подій для події кліку.
if self.curPack < #packs then
 local right = Bitmap.new(Texture.new("images/right.png", true))
 local rightButton = Button.new(right)
 rightButton:setPosition(conf.dx + conf.width - rightButton:getWidth(), conf.dy + conf.height - rightButton:getHeight())
 self:addChild(rightButton)
 rightButton:addEventListener("click", self.nextPack, self)
end
Давайте додамо previous pack кнопку дуже схожим чином. Перевірте, чи це не перший пак. Потім створіть об'єкт Bitmap з зображення  стрілки зліва та створіть з нього кнопку. Помістіть кнопку в лівому нижньому куті та додайте її до сцени. В кінці, додати метод prevPack () як слухача події для натискання події нашої кнопки.

if self.curPack > 1 then
 local left = Bitmap.new(Texture.new("images/left.png", true))
 local leftButton = Button.new(left)
 leftButton:setPosition(-conf.dx, conf.dy + conf.height - leftButton:getHeight())
 self:addChild(leftButton)
 leftButton:addEventListener("click", self.prevPack, self)
end
Це все, перевірте це, запустивши проект і натиснувши кнопку " Start Game", ви повинні побачити чудовий ефект переходу під час перемикання між пакетами.

Створення класу GameManager


Далі ми повинні керувати нашими рівнями. Ми повинні мати можливість перевірити, які заблоковані і які можуть отримати доступ до користувачів. Для цього ми створимо клас GameManager, який постійно зберігатиме інформацію про рівні, розблокований і який балл користувач досяг на цьому рівні, і коли користувач це зробив.

Тому створіть новий файл з назвою GameManager.lua у нашій папці classes і створіть у ньому клас з назвою GameManager.

GameManager = Core.class()
Потім ми визначаємо метод init для класу GameManager і створюємо порожню
таблицю властивості self.packs

function GameManager:init()
 self.packs = {}
end
Після цього нам потрібен спосіб завантаження інформації про раніше збережений пакет.
Коли ми будемо читати це кожен раз, коли ми відвідаємо сцену levelselect, створіть менші файли, що представляють кожну упаковку окремо, а не зберігати все в одному файлі.
Таким чином, ми надамо номер пакету для методу loadPack ().
Ми перевіримо, чи у нас ще немає інформації про цей пакет, нам потрібно завантажити його за допомогою DataSaver і надати шлях до каталогу Documents для збереження файлів, так само, як ми обговорили в попередньому розділі під час реалізації класу Settings. Після завантаження ми повинні знову перевірити, і якщо ще немає інформації про цей пакет,
ми просто створюємо порожню таблицю.

function GameManager:loadPack(pack)
 if self.packs[pack] == nil then
 self.packs[pack] = dataSaver.load("|D|scores"..pack)
 end
 if self.packs[pack] == nil then
 self.packs[pack] = {}
 end
end
Далі давайте визначимо простий метод збереження певних пакетів. Знову ж таки, ми просто передаємо номер pack, і DataSaver зберігає будь-яку інформацію, яку ми зберігаємо для цього pack.

function GameManager:save(pack)
 dataSaver.save("|D|scores"..pack, self.packs[pack])
end
Наступний спосіб створить інформацію про новий level, який не мав інформація раніше. Тут ми просто створюємо нову таблицю для цього конкретного level в цьому конкретному пакеті, і ми можемо встановити будь-які початкові значення, які ми хочемо відслідковувати, а потім надавати встановлювачам та геттерам для кожного значення. Врешті-решт, ми просто зберігаємо інформацію про цей пакет.

function GameManager:createLevel(pack, level)
 self.packs[pack][level] = {}
 self.packs[pack][level].score = 0
 self.packs[pack][level].time = nil
 self.packs[pack][level].unlocked = false
 self:save(pack)
end
Тепер, коли ми маємо всі допоміжні методи, давайте створимо метод, який буде перевіряти, чи рівень розблоковано чи ні. Таким чином, ми надамо пакет і номер рівня за допомогою цього методу. Вона спочатку спробує завантажити інформацію, яку вона має про цей пакет, тоді, якщо немає інформації про цей конкретний рівень, що зберігається, ми просто створюємо інформацію про новий рівень. Після цього ми просто перевіримо, чи рівень розблоковано, чи ні, перевіривши одне з його властивостей, назване unlocked


function GameManager:isUnlocked(pack, level)
 self:loadPack(pack)
 if(self.packs[pack][level] == nil) then
 self:createLevel(pack, level)
 end
 return self.packs[pack][level].unlocked
end
Нам також потрібен метод розблокування певного рівня. Знову ж таки, ми надаємо номер пакунка та рівня як параметри. Потім ми спробуємо завантажити інформацію про пакет і створити цей рівень, якщо його ще не існує. Потім ми перевіряємо, чи цей рівень ще не розблоковано, встановіть його unlocked прапор на true та збережіть цей пакет.

function GameManager:unlockLevel(pack, level)
 self:loadPack(pack)
 if(self.packs[pack][level] == nil) then
 self:createLevel(pack, level)
 end
 if not self.packs[pack][level].unlocked then
 self.packs[pack][level].unlocked = true
 self:save(pack)
 end
end
Ось найцікавіший метод, який автоматично отримає наступний рівень.
Також потрібно мати можливість отримати перший рівень наступного пакету, якщо поточний рівень був останнім рівнем поточного пакету. Крім того, ми можемо зробити це, щоб розблокувати наступний рівень.
Цей метод прийме поточні номери пакетів і рівнів, а також розблокування параметра, який повинен вказати, чи нам потрібно розблокувати наступний рівень.
Тепер, як завжди, давайте завантажувати інформацію про пакет. Потім ми намагаємося збільшити номер рівня і перевірити, чи підвищений номер рівня перевищує загальну кількість рівнів у цьому пакеті (значення якого можна дізнатись у packs.lua),
то ми встановлюємо рівень як перший і збільшуємо номер пачки.
Як останнє, ми перевіряємо, чи доступні нові значення пакетів та рівнів у файлі packs.lua. Якщо так, ми просто розблокуємо цей рівень, якщо нам потрібно, і поверніть номер пакету та рівня, але якщо він недоступний у файлі packs.lua. це означатиме, що ми перевищили визначені пакети і дійшли до кінця гри, тому ми просто повернемо нульові значення.


function GameManager:getNextLevel(pack, level, unlock)
 self:loadPack(pack)
 level = level + 1
 if packs[pack].levels < level then
 level = 1
 pack = pack + 1
 end
 if packs[pack] and packs[pack].levels >= level then
 if unlock then
 self:unlockLevel(pack, level)
 end
 return pack, level
 else
 return nil, nil
 end
end

Логіка розблокування рівнів

Тепер, коли у нас є наш клас GameManager, давайте використовувати його на нашій сцені levelselect.
Є два способи зробити це тут,

  • або створивши загальний екземпляр класу GameManager всередині main.lua і використовуючи його впродовж усього проекту, 
  • або створивши локальний екземпляр всередині певних сцен і використовуючи їх при необхідності. 
Як приклад ,якщо в кожній упаковці є велика кількість пакетів і у кожному пакеті є багато рівнів, вам слід створити локальний екземпляр для кожної окремої сцени.

У випадку глобального об'єкта, це означатиме, що ви будете зберігати інформацію про всі завантажені рівні всередині вашої пам'яті. Але, з іншого боку, вам не доведеться перезавантажувати інформацію про кожен рівень, що може покращити час завантаження.
Але насправді інформація, що зберігається на рівні, набагато менша, і більш зручно мати єдине глобальне посилання, ніж створювати нові об'єкти щоразу, коли це потрібно.
Отже, перейдіть до main.lua і створіть глобальний екземпляр класу GameManager


gm = GameManager.new()
Потім перейдіть до levelselectScene.lua, і всередині методу init - розблокувати перший рівень першого пакета, оскільки ми хочемо, щоб користувач почав грати з ним. Таким чином, ми перевіряємо, чи поточний пакет є першим пакунком, і якщо перший рівень не розблоковано, ми його розблокуємо

if self.curPack == 1 and not gm:isUnlocked(1, 1) then
 gm:unlockLevel(1, 1)
end
Потім, усередині нашого циклу, який створює сітку, нам потрібно перевірити кожен рівень, якщо він заблоковано, ми залишаємо це так, як є, але якщо він розблокований, ми надаємо йому інший малюнок, створивши Button  з іншим зображенням Bitmap , зберігаючи поточний ID рівня як властивість нашого екземпляра та додаючи обробник подій,
до якого ми передаємо екземпляр Button   як перший параметр. В обробнику події ми просто встановлюємо curLevel в наших налаштуваннях до значення наданого ID та переходять на сцену level.


--створити зображення рівня
local level
if gm:isUnlocked(self.curPack, i) then
 local bitmap = Bitmap.new(Texture.new("images/level_unlocked.png", true))
 level = Button.new(bitmap)
 level.id = i
 level:addEventListener("click", function(self) sets:set("curLevel", self.id)
 sceneManager:changeScene("level", conf.transitionTime, conf.transition, conf.easing) end, level)
else
 level = Bitmap.new(Texture.new("images/level_locked.png", true))
end
level:setPosition(currentX, currentY)
grid:addChild(level)
За допомогою методу LevelScene: init () ми можемо читати значення curPack та curLevel з класу налаштувань, щоб визначити, який рівень користувач хоче завантажити зараз. Отже, давайте перейдемо до LevelScene.lua і змінюємо його метод init для зберігання поточної інформації про пакет і рівень як властивості екземпляра.

self.curPack = sets:get("curPack")
self.curLevel = sets:get("curLevel")

Читання карти рівня


Наступним для нас завданням є створення файлу карти   рівня для кожного level. Потім прочитайте його всередині level.lua на основі того, який рівень ми зараз хочемо завантажити, і помістіть всі об'єкти відповідно до карти рівня.
Формат карти рівня може залежити від того, що ви використовуєте для створення рівнів.
Ви можете використовувати стороннє програмне забезпечення, який згенерує вам карту автоматично, і вам потрібно буде лише інтерпретувати його в Gideros.

У нашому випадку, оскільки на прикладі рівнів буде міститися невелика кількість об'єктів, ми навіть зможемо редагувати її вручну в будь-якому текстовому редакторі без особливого плутанини. Карта рівня буде звичайним файлом JSON з іменами об'єктів та координатами.
Тому створіть папку levels усередині Gideros Studio та створіть і ній  файл 1-1.json (перший номер, що вказує пакет, друге число, що вказує рівень). У цій карті рівня ми  визначимо тип об'єкта (основний або дотик) та його розташування на рівні (координати x та y).

[{"type":"main","x":400,"y":150}, {"type":"touch","x":400,"y":400}]
Крім того, ви можете визначити розміри, обертання та інші властивості у карті рівня і тлумачити їх всередині вашого проекту Gideros.
Давайте додамо ще пару рівнів, щоб перевірити функціональність. Тому створіть 1-2.json з деякими основними та клікабельними м'ячами.

[{"type":"main","x":400,"y":150},{"type":"touch","x":250,"y":400} {"type":"touch","x":525,"y":400}]
Давайте також створимо третій рівень 1-3.json з різними клікабельними  кульками .

[{"type":"main","x":400,"y":150},{"type":"touch","x":250,"y":225},
 {"type":"touch","x":600,"y":60}]
Тепер давайте додамо ці файли до папки levels усередині Gideros Studio і спробуємо прочитати його в методі Levelscene: init () у levels.lua.

Спочатку ми видалимо код, де ми просто створюємо тестові об'єкти MainBall та TouchBall, а замість цього завантажуємо карту рівня, виходячи з параметрів curPack і curLevel. Потім ми створюємо властивість екземпляра self.ballsLeft, де ми будемо зберігати, скільки м'ячів залишилося вдарити.

self.curPack = sets:get("curPack")
self.curLevel = sets:get("curLevel")
self.level = dataSaver.load("levels/"..self.curPack.."- "..self.curLevel)
self.ballsLeft = 0
Потім ми проходимо всі об'єкти, визначені в карті рівня, і створюємо ці об'єкти в нашій грі. Якщо це основний тип, ми створюємо MainBall з вказаними координатами.
Те саме стосується TouchBall, тільки ми додатково підраховуємо кількість TouchBalls в грі.

for i, value in ipairs(self.level) do
 if value.type == "main" then
 self.mainBall = MainBall.new(self, value.x, value.y)
 self:addChild(self.mainBall)
 elseif value.type == "touch" then
 local touch = TouchBall.new(self, value.x, value.y)
 self:addChild(touch)
 self.ballsLeft = self.ballsLeft + 1
 end
end

Завершення рівня


Тепер створіть метод LevelScene: completed (), де ми б розблокували наступний рівень і представили  діалог, щоб перемістити користувачів на цей  розблокований рівень.
У методі LevelScene: completed () ми спробуємо отримати наступний рівень від GameManager. Якщо наступний рівень або пакет nil, це означає, що ми досягли кінця гри,
таким чином, ми будемо відображати відповідне повідомлення і перейдемо до StartScene.
Якщо є наступний рівень, ми будемо зберігати його в класі Settings , щоб ми могли отримати його на наступному рівні завантаження, потім відобразити відповідне повідомлення і переходити на ту ж саму сцену, яка буде завантажувати карту рівня рівня та упаковки номер зберігається в класі Settings .
Після завершення ми покажемо об'єкт AlertDialog, який в Gideros являє собою простий  діалог із повідомленням; як виглядає, залежить від платформи, на якій вона використовується..

function LevelScene:completed()
 self.curPack, self.curLevel = gm:getNextLevel(self.curPack,
 self.curLevel, true)
 --якщо curPack == nil це означає, що ми досягли кінця гри
 if self.curPack == nil then
 local dialog = AlertDialog.new("Гра закінчена", "Ви пройшли гру", "ок")
 dialog:addEventListener(Event.COMPLETE, function()
 --йти в головне sceneManager
 sceneManager:changeScene("Старт", conf.transitionTime, conf.transition, conf.easing)
 end)
 dialog:show()
 else
 -- ми будемо зберігати нові ідентифікатори пакетів і рівнів у settings
 sets:set("curPack", self.curPack)
 sets:set("curLevel", self.curLevel)
 local dialog = AlertDialog.new("Рівень завершено", "Перейти на наступний рівень", "OK")
 dialog:addEventListener(Event.COMPLETE, function()
 sceneManager:changeScene("Рівень", conf.transitionTime, conf.transition, conf.easing)
 end)
 dialog:show()
 end
end
Тепер нам потрібно лише визначити, коли рівень буде завершено. Саме тому ми створили властивість self.ballsLeft, щоб ми могли відняти від нього 1 кожен раз, коли ми потрапили в новий м'яч, і якщо він дорівнював 0 , ми б знали, що ми пройшли рівень. Тож давайте додамо його до обробника подій зіткнень

--смайлик-м'яч
bodyB.object:smile()
bodyA.type = nil
self.ballsLeft = self.ballsLeft - 1
if self.ballsLeft == 0 then
 self:completed()
end
І це, ми можемо визначити будь-яку кількість рівнів, які ми хочемо, просто не забувайте оновлювати packs.lua  відповідно

Підсумок:

Тепер ми реалізували нашу основну логіку гри, і ми можемо визначати кілька пакетів і рівнів, а також створювати, читати та інтерпретувати визначення рівня та створювати різні об'єкти гри як окремі класи Gideros. Крім того, ми можемо керувати розблокуванням рівнів після завершення попереднього рівня.

У наступному розділі ми зробимо нашу гру більш просунутою та відполірованою, додаючи анімацію та звукові ефекти, а також змінюючи, як ми взаємодіємо з грою та його елементами керування, і зробити гру більш цікавою , додаючи рахунок.












Немає коментарів:

Дописати коментар