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

Опубликовано: 2022-03-11

Это вторая часть полного руководства по языку обработки. В первой части я дал базовое пошаговое руководство по языку обработки. Следующим шагом в изучении Processing является просто более практическое программирование.

В этой статье я покажу вам, как использовать Processing для реализации вашей собственной игры, шаг за шагом. Каждый шаг будет подробно объяснен. Затем мы портируем игру в сеть.

Создайте простую игру с языком обработки.

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

Учебник по обработке: простая игра

Игра, которую мы создадим в этом уроке по обработке, представляет собой комбинацию Flappy Bird, Pong и Brick Breaker. Причина, по которой я выбрал такую ​​игру, заключается в том, что в ней есть большинство концепций, с которыми сталкиваются новички при изучении разработки игр. Это основано на моем опыте, когда я был ассистентом преподавателя, помогая новым программистам научиться использовать Processing. Эти концепции включают гравитацию, столкновения, ведение счета, работу с различными экранами и взаимодействие с клавиатурой и мышью. Во Flappy Pong есть все.

Сыграйте в игру прямо сейчас!

Без использования концепций объектно-ориентированного программирования (ООП) непросто создавать сложные игры, такие как платформеры с несколькими уровнями, игроками, сущностями и т. д. По мере продвижения вперед вы увидите, как очень быстро усложняется код. Я сделал все возможное, чтобы этот учебник по обработке был организованным и простым.

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

Итак, начнем.

Создание Flappy Pong

Учебник по обработке Шаг № 1: Инициализация и обработка различных экранов

Первым шагом является инициализация нашего проекта. Для начала напишем нашу настройку и отрисуем блоки, как обычно, ничего необычного или нового. Затем мы будем обрабатывать различные экраны (начальный экран, экран игры, экран завершения игры и т. д.). Итак, возникает вопрос, как сделать так, чтобы Processing показывал правильную страницу в нужное время?

Выполнить эту задачу достаточно просто. У нас будет глобальная переменная, в которой хранится информация об активном в данный момент экране. Затем мы рисуем содержимое правильного экрана в зависимости от переменной. В блоке рисования у нас будет оператор if, который проверяет переменную и соответственно отображает содержимое экрана. Всякий раз, когда мы хотим изменить экран, мы изменим эту переменную на идентификатор экрана, который мы хотим, чтобы он отображал. С учетом сказанного, вот как выглядит наш скелетный код:

 /********* VARIABLES *********/ // We control which screen is active by settings / updating // gameScreen variable. We display the correct screen according // to the value of this variable. // // 0: Initial Screen // 1: Game Screen // 2: Game-over Screen int gameScreen = 0; /********* SETUP BLOCK *********/ void setup() { size(500, 500); } /********* DRAW BLOCK *********/ void draw() { // Display the contents of the current screen if (gameScreen == 0) { initScreen(); } else if (gameScreen == 1) { gameScreen(); } else if (gameScreen == 2) { gameOverScreen(); } } /********* SCREEN CONTENTS *********/ void initScreen() { // codes of initial screen } void gameScreen() { // codes of game screen } void gameOverScreen() { // codes for game over screen } /********* INPUTS *********/ public void mousePressed() { // if we are on the initial screen when clicked, start the game if (gameScreen==0) { startGame(); } } /********* OTHER FUNCTIONS *********/ // This method sets the necessary variables to start the game void startGame() { gameScreen=1; }

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

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

В части void mousePressed(){...} мы прослушиваем щелчки мыши, и если активный экран равен 0, то это начальный экран, мы вызываем метод startGame() , который запускает игру, как и следовало ожидать. Первая строка этого метода изменяет переменную gameScreen на 1, экран игры.

Если это понятно, следующим шагом будет реализация нашего начального экрана. Для этого мы будем редактировать метод initScreen() . Вот оно:

 void initScreen() { background(0); textAlign(CENTER); text("Click to start", height/2, width/2); }

Теперь наш начальный экран имеет черный фон и простой текст «Нажмите, чтобы начать», расположенный посередине и выровненный по центру. Но когда мы нажимаем, ничего не происходит. Мы еще не указали какой-либо контент для нашего игрового экрана. В методе gameScreen() ничего нет, поэтому мы не покрываем предыдущее содержимое, нарисованное на последнем экране (текст), используя background() в качестве первой строки отрисовки. Вот почему текст все еще там, даже несмотря на то, что строка text() больше не вызывается (точно так же, как в примере с движущимся мячом из последней части, который оставлял за собой след) . Фон по-прежнему черный по той же причине. Итак, давайте приступим к реализации игрового экрана.

 void gameScreen() { background(255); }

После этого изменения вы заметите, что фон стал белым, а текст исчез.

Учебное пособие по обработке, шаг № 2: создание шара и реализация гравитации

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

Во-первых, добавим следующее:

 ... int ballX, ballY; int ballSize = 20; int ballColor = color(0); ... void setup() { ... ballX=width/4; ballY=height/5; } ... void gameScreen() { ... drawBall(); } ... void drawBall() { fill(ballColor); ellipse(ballX, ballY, ballSize, ballSize); }

Мы определили координаты как глобальные переменные, создали метод, рисующий мяч, вызываемый из метода gameScreen . Единственное, на что здесь следует обратить внимание, это то, что мы инициализировали координаты, но определили их в setup() . Причина, по которой мы это сделали, заключается в том, что мы хотели, чтобы мяч начинался с одной четвертой слева и одной пятой сверху. Нет особой причины, по которой мы этого хотим, но это хороший момент для старта мяча. Поэтому нам нужно было получить width и height эскиза динамически. Размер эскиза определяется в setup() после первой строки. width и height не устанавливаются перед setup() , поэтому мы не смогли бы добиться этого, если бы определили переменные сверху.

Сила тяжести

Теперь реализация гравитации на самом деле является легкой частью. Есть только несколько трюков. Вот первая реализация:

 ... float gravity = 1; float ballSpeedVert = 0; ... void gameScreen() { ... applyGravity(); keepInScreen(); } ... void applyGravity() { ballSpeedVert += gravity; ballY += ballSpeedVert; } void makeBounceBottom(float surface) { ballY = surface-(ballSize/2); ballSpeedVert*=-1; } void makeBounceTop(float surface) { ballY = surface+(ballSize/2); ballSpeedVert*=-1; } // keep ball in the screen void keepInScreen() { // ball hits floor if (ballY+(ballSize/2) > height) { makeBounceBottom(height); } // ball hits ceiling if (ballY-(ballSize/2) < 0) { makeBounceTop(0); } }

И результат:

Мяч, бесконечно подпрыгивающий под действием псевдогравитации.

Придержи лошадей, физик. Я знаю, что в реальной жизни гравитация работает иначе. Наоборот, это скорее процесс анимации, чем что-либо еще. Переменная, которую мы определили как gravity , представляет собой просто числовое значение — число с float , поэтому мы можем использовать десятичные значения, а не только целые числа, — которое мы добавляем к ballSpeedVert в каждом цикле. А ballSpeedVert — это вертикальная скорость мяча, которая прибавляется к координате Y мяча ( ballY ) на каждом цикле. Следим за координатами мяча и следим за тем, чтобы он оставался на экране. Если бы мы этого не сделали, мяч бы упал до бесконечности. Пока наш мяч движется только вертикально. Итак, мы наблюдаем границы пола и потолка экрана. С помощью метода keepInScreen() мы проверяем, меньше ли ballY ( + радиус) height , и аналогично ballY ( - радиус) больше 0 . Если условия не выполняются, мы заставляем мяч отскакивать (снизу или вверх) с makeBounceBottom() и makeBounceTop() . Чтобы заставить мяч отскакивать, мы просто перемещаем мяч точно в то место, где он должен был отскочить, и умножаем вертикальную скорость ( ballSpeedVert ) на -1 (умножение на -1 меняет знак). Когда значение скорости имеет знак минус, при добавлении координаты Y скорость становится ballY + (-ballSpeedVert) , что равно ballY - ballSpeedVert . Таким образом, мяч сразу меняет свое направление с той же скоростью. Затем, когда мы добавляем gravity к ballSpeedVert а ballSpeedVert имеет отрицательное значение, оно начинает приближаться к 0 , в конечном итоге становится равным 0 и снова начинает увеличиваться. Это заставляет мяч подниматься, подниматься медленнее, останавливаться и начинать падать.

Мяч, бесконечно прыгающий на ракетке.

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

 ... float airfriction = 0.0001; float friction = 0.1; ... void applyGravity() { ... ballSpeedVert -= (ballSpeedVert * airfriction); } void makeBounceBottom(int surface) { ... ballSpeedVert -= (ballSpeedVert * friction); } void makeBounceTop(int surface) { ... ballSpeedVert -= (ballSpeedVert * friction); }

И теперь наш процесс анимации производит это:

Мяч подпрыгивает, но останавливается из-за трения.

Как следует из названия, friction — это поверхностное трение, а airfriction — трение воздуха. Таким образом, очевидно, что friction должно возникать каждый раз, когда мяч касается любой поверхности. Однако airfriction должно применяться постоянно. Вот что мы сделали. applyGravity() запускается в каждом цикле, поэтому мы забираем 0.0001 % его текущего значения из ballSpeedVert в каждом цикле. makeBounceBottom() и makeBounceTop() запускаются, когда мяч касается любой поверхности. Итак, в этих методах мы сделали то же самое, только на этот раз с friction .

Учебник по обработке Шаг № 3: Создание ракетки

Теперь нам нужна ракетка, на которой будет прыгать мяч. Мы должны контролировать рэкет. Давайте сделаем его управляемым с помощью мыши. Вот код:

 ... color racketColor = color(0); float racketWidth = 100; float racketHeight = 10; ... void gameScreen() { ... drawRacket(); ... } ... void drawRacket(){ fill(racketColor); rectMode(CENTER); rect(mouseX, mouseY, racketWidth, racketHeight); }

Мы определили цвет, ширину и высоту ракетки как глобальную переменную, мы могли бы захотеть, чтобы они менялись во время игры. Мы реализовали метод drawRacket() , который делает то, что предполагает его название. Мы устанавливаем rectMode в центр, поэтому наша ракетка выровнена по центру нашего курсора.

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

 ... int racketBounceRate = 20; ... void gameScreen() { ... watchRacketBounce(); ... } ... void watchRacketBounce() { float overhead = mouseY - pmouseY; if ((ballX+(ballSize/2) > mouseX-(racketWidth/2)) && (ballX-(ballSize/2) < mouseX+(racketWidth/2))) { if (dist(ballX, ballY, ballX, mouseY)<=(ballSize/2)+abs(overhead)) { makeBounceBottom(mouseY); // racket moving up if (overhead<0) { ballY+=overhead; ballSpeedVert+=overhead; } } } }

И вот результат:

Мяч отскакивает от ракетки, но останавливается из-за трения.

Итак, что watchRacketBounce() , так это гарантирует, что ракетка и мяч столкнутся. Здесь нужно проверить две вещи: выровнены ли мяч и ракетка как по вертикали, так и по горизонтали. Первый оператор if проверяет, больше ли координата X правой стороны мяча, чем координата X левой стороны ракетки (и наоборот). Если это так, второе утверждение проверяет, меньше ли расстояние между мячом и ракеткой радиуса мяча или равно ему (что означает, что они сталкиваются) . Итак, если эти условия выполняются, вызывается метод makeBounceBottom() , и мяч отскакивает от нашей ракетки (в mouseY , где находится ракетка).

Вы заметили переменную overhead информацию, которая вычисляется с помощью mouseY - pmouseY ? pmouseX и pmouseY хранят координаты мыши в предыдущем кадре. Поскольку мышь может двигаться очень быстро, есть большая вероятность, что мы не сможем правильно определить расстояние между мячом и ракеткой между кадрами, если мышь движется к мячу достаточно быстро. Итак, мы берем разницу координат мыши между кадрами и учитываем ее при определении расстояния. Чем быстрее движется мышь, тем большее расстояние допустимо.

Мы также используем overhead по другой причине. Мы определяем, в какую сторону движется мышь, проверяя знак overhead . Если накладные расходы отрицательные, то в предыдущем кадре мышь находилась где-то внизу, поэтому наша мышь (ракетка) движется вверх. В этом случае мы хотим добавить мячу дополнительную скорость и переместить его немного дальше обычного отскока, чтобы имитировать эффект удара по мячу ракеткой. Если значение overhead меньше 0 , мы добавляем его к ballY и ballSpeedVert , чтобы мяч летел выше и быстрее. Таким образом, чем быстрее ракетка ударит по мячу, тем выше и быстрее он поднимется вверх.

Учебное пособие по обработке данных, шаг № 4: горизонтальное движение и контроль мяча

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

 ... // we will start with 0, but for we give 10 just for testing float ballSpeedHorizon = 10; ... void gameScreen() { ... applyHorizontalSpeed(); ... } ... void applyHorizontalSpeed(){ ballX += ballSpeedHorizon; ballSpeedHorizon -= (ballSpeedHorizon * airfriction); } void makeBounceLeft(float surface){ ballX = surface+(ballSize/2); ballSpeedHorizon*=-1; ballSpeedHorizon -= (ballSpeedHorizon * friction); } void makeBounceRight(float surface){ ballX = surface-(ballSize/2); ballSpeedHorizon*=-1; ballSpeedHorizon -= (ballSpeedHorizon * friction); } ... void keepInScreen() { ... if (ballX-(ballSize/2) < 0){ makeBounceLeft(0); } if (ballX+(ballSize/2) > width){ makeBounceRight(width); } }

И результат:

Мяч подпрыгивает теперь также горизонтально.

Идея здесь та же, что и для вертикального движения. Мы создали переменную горизонтальной скорости ballSpeedHorizon . Мы создали метод для приложения горизонтальной скорости к ballX и устранения трения о воздух. Мы добавили еще два оператора if в метод keepInScreen() , который будет отслеживать попадание мяча в левый и правый края экрана. Наконец, мы создали makeBounceLeft() и makeBounceRight() для обработки отскоков слева и справа.

Теперь, когда мы добавили в игру горизонтальную скорость, мы хотим управлять мячом с помощью ракетки. Как и в знаменитой игре Breakout от Atari и во всех других играх с разбиванием кирпичей, мяч должен лететь влево или вправо в зависимости от точки на ракетке, в которую он попадает. Края ракетки должны придавать мячу большую горизонтальную скорость, в то время как середина не должна оказывать никакого влияния. Сначала код:

 void watchRacketBounce() { ... if ((ballX+(ballSize/2) > mouseX-(racketWidth/2)) && (ballX-(ballSize/2) < mouseX+(racketWidth/2))) { if (dist(ballX, ballY, ballX, mouseY)<=(ballSize/2)+abs(overhead)) { ... ballSpeedHorizon = (ballX - mouseX)/5; ... } } }

Результат:

Горизонтальная физика в стиле прорыва.

Добавление этой простой строки в watchRacketBounce() . Что мы сделали, так это определили расстояние от центра ракетки до точки удара мяча с помощью ballX - mouseX . Затем мы делаем его горизонтальной скоростью. Фактическая разница была слишком большой, поэтому я попробовал несколько раз и решил, что одна десятая значения кажется наиболее естественной.

Учебник по обработке Шаг № 5: Создание стен

Наш скетч с каждым шагом все больше походит на игру. На этом этапе мы добавим стены, движущиеся влево, как в Flappy Bird:

 ... int wallSpeed = 5; int wallInterval = 1000; float lastAddTime = 0; int minGapHeight = 200; int maxGapHeight = 300; int wallWidth = 80; color wallColors = color(0); // This arraylist stores data of the gaps between the walls. Actuals walls are drawn accordingly. // [gapWallX, gapWallY, gapWallWidth, gapWallHeight] ArrayList<int[]> walls = new ArrayList<int[]>(); ... void gameScreen() { ... wallAdder(); wallHandler(); } ... void wallAdder() { if (millis()-lastAddTime > wallInterval) { int randHeight = round(random(minGapHeight, maxGapHeight)); int randY = round(random(0, height-randHeight)); // {gapWallX, gapWallY, gapWallWidth, gapWallHeight} int[] randWall = {width, randY, wallWidth, randHeight}; walls.add(randWall); lastAddTime = millis(); } } void wallHandler() { for (int i = 0; i < walls.size(); i++) { wallRemover(i); wallMover(i); wallDrawer(i); } } void wallDrawer(int index) { int[] wall = walls.get(index); // get gap wall settings int gapWallX = wall[0]; int gapWallY = wall[1]; int gapWallWidth = wall[2]; int gapWallHeight = wall[3]; // draw actual walls rectMode(CORNER); fill(wallColors); rect(gapWallX, 0, gapWallWidth, gapWallY); rect(gapWallX, gapWallY+gapWallHeight, gapWallWidth, height-(gapWallY+gapWallHeight)); } void wallMover(int index) { int[] wall = walls.get(index); wall[0] -= wallSpeed; } void wallRemover(int index) { int[] wall = walls.get(index); if (wall[0]+wall[2] <= 0) { walls.remove(index); } }

И это привело к:

Мяч, прыгающий через уровень со стенами.

Несмотря на то, что код выглядит длинным и пугающим, я обещаю, что в нем нет ничего сложного для понимания. Первое, на что следует обратить внимание, это ArrayList . Для тех из вас, кто не знает, что такое ArrayList , скажу, что это просто реализация списка, которая действует как массив, но имеет перед ним некоторые преимущества. Его размер можно изменить, он имеет полезные методы, такие как list.add(index) , list.get(index) и list.remove(index) . Мы храним данные стен в виде целочисленных массивов в массиве массивов. Данные, которые мы храним в массивах, относятся к зазору между двумя стенами. Массивы содержат следующие значения:

 [gap wall X, gap wall Y, gap wall width, gap wall height]

Фактические стены рисуются на основе значений стены зазора. Обратите внимание, что все это можно было бы обрабатывать лучше и чище с помощью классов, но, поскольку использование объектно-ориентированного программирования (ООП) не входит в рамки этого руководства по обработке, мы будем обрабатывать его именно так. У нас есть два базовых метода для управления стенами: wallAdder() и wallHandler .

wallAdder() просто добавляет новые стены в массив через каждую миллисекунду wallInterval . У нас есть глобальная переменная lastAddTime , в которой хранится время добавления последней стены (в миллисекундах) . Если текущая миллисекунда millis() минус последняя добавленная миллисекунда lastAddTime больше, чем значение нашего интервала wallInterval , это означает, что пришло время добавить новую стену. Затем генерируются случайные гэп-переменные на основе глобальных переменных, определенных в самом верху. Затем в список массивов добавляется новая стена (целочисленный массив, в котором хранятся данные стены зазора), а lastAddTime устанавливается на текущую миллисекунду millis() .

wallHandler() перебирает текущие стены, которые находятся в списке массивов. И для каждого элемента в каждом цикле он вызывает wallRemover(i) , wallMover(i) и wallDrawer(i) по значению индекса массива. Эти методы делают то, что следует из их названия. wallDrawer() рисует фактические стены, основываясь на данных стен промежутка. Он берет массив данных стены из списка массивов и вызывает метод rect() , чтобы нарисовать стены там, где они должны быть. wallMover() берет элемент из списка массивов, изменяет его местоположение по оси X на основе глобальной переменной wallSpeed . Наконец, wallRemover() удаляет стены из массива, которые находятся за пределами экрана. Если бы мы этого не сделали, Processing обработал бы их так, как будто они все еще находятся на экране. И это было бы огромной потерей производительности. Поэтому, когда стена удаляется из массива, она не рисуется в последующих циклах.

Последнее, что осталось сделать, это обнаружить столкновения между мячом и стенами.

 void wallHandler() { for (int i = 0; i < walls.size(); i++) { ... watchWallCollision(i); } } ... void watchWallCollision(int index) { int[] wall = walls.get(index); // get gap wall settings int gapWallX = wall[0]; int gapWallY = wall[1]; int gapWallWidth = wall[2]; int gapWallHeight = wall[3]; int wallTopX = gapWallX; int wallTopY = 0; int wallTopWidth = gapWallWidth; int wallTopHeight = gapWallY; int wallBottomX = gapWallX; int wallBottomY = gapWallY+gapWallHeight; int wallBottomWidth = gapWallWidth; int wallBottomHeight = height-(gapWallY+gapWallHeight); if ( (ballX+(ballSize/2)>wallTopX) && (ballX-(ballSize/2)<wallTopX+wallTopWidth) && (ballY+(ballSize/2)>wallTopY) && (ballY-(ballSize/2)<wallTopY+wallTopHeight) ) { // collides with upper wall } if ( (ballX+(ballSize/2)>wallBottomX) && (ballX-(ballSize/2)<wallBottomX+wallBottomWidth) && (ballY+(ballSize/2)>wallBottomY) && (ballY-(ballSize/2)<wallBottomY+wallBottomHeight) ) { // collides with lower wall } }

watchWallCollision() для каждой стены в каждом цикле. Мы берем координаты стены зазора, вычисляем координаты реальных стен (верхней и нижней) и проверяем, не сталкиваются ли координаты шара со стенками.

Учебное пособие по обработке, шаг № 6: здоровье и оценка

Теперь, когда мы можем обнаружить столкновения мяча и стен, мы можем определиться с игровой механикой. После некоторой настройки игры мне удалось сделать игру более играбельной. Но все равно было очень тяжело. Моей первой мыслью об игре было сделать ее похожей на Flappy Bird, когда мяч касается стен, игра заканчивается. Но потом я понял, что играть будет невозможно. Итак, вот что я подумал:

Над мячом должна быть шкала здоровья. Мяч должен терять здоровье, пока он касается стен. С этой логикой нет смысла заставлять мяч отскакивать от стен. Поэтому, когда здоровье равно 0, игра должна закончиться, и мы должны переключиться на экран окончания игры. Итак, приступим:

 int maxHealth = 100; float health = 100; float healthDecrease = 1; int healthBarWidth = 60; ... void gameScreen() { ... drawHealthBar(); ... } ... void drawHealthBar() { // Make it borderless: noStroke(); fill(236, 240, 241); rectMode(CORNER); rect(ballX-(healthBarWidth/2), ballY - 30, healthBarWidth, 5); if (health > 60) { fill(46, 204, 113); } else if (health > 30) { fill(230, 126, 34); } else { fill(231, 76, 60); } rectMode(CORNER); rect(ballX-(healthBarWidth/2), ballY - 30, healthBarWidth*(health/maxHealth), 5); } void decreaseHealth(){ health -= healthDecrease; if (health <= 0){ gameOver(); } }

А вот и простой запуск:

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

Мы создали глобальную переменную health , чтобы сохранить здоровье мяча. А затем создал метод drawHealthBar() , который рисует два прямоугольника поверх мяча. Первая — это базовая полоса здоровья, другая — активная, показывающая текущее здоровье. Ширина второго является динамической и рассчитывается с помощью healthBarWidth*(health/maxHealth) , отношения нашего текущего здоровья к ширине полосы здоровья. Наконец, цвета заливки устанавливаются в соответствии со значением здоровья. И последнее, но не менее важное: баллы :

 ... void gameOverScreen() { background(0); textAlign(CENTER); fill(255); textSize(30); text("Game Over", height/2, width/2 - 20); textSize(15); text("Click to Restart", height/2, width/2 + 10); } ... void wallAdder() { if (millis()-lastAddTime > wallInterval) { ... // added another value at the end of the array int[] randWall = {width, randY, wallWidth, randHeight, 0}; ... } } void watchWallCollision(int index) { ... int wallScored = wall[4]; ... if (ballX > gapWallX+(gapWallWidth/2) && wallScored==0) { wallScored=1; wall[4]=1; score(); } } void score() { score++; } void printScore(){ textAlign(CENTER); fill(0); textSize(30); text(score, height/2, 50); }

Нам нужно было забить, когда мяч проходит стенку. Но нам нужно добавить максимум 1 балл на стену. Это означает, что если мяч проходит через стену, затем возвращается и снова проходит через нее, еще один счет не должен добавляться. Для этого мы добавили еще одну переменную в массив стен промежутка в массиве массивов. Новая переменная хранит 0 , если мяч еще не прошел эту стену, и 1 , если это произошло. Затем мы изменили метод watchWallCollision() . Мы добавили условие, которое запускает метод score() и помечает стену как пройденную, когда мяч проходит стену, которую он не прошел раньше.

Сейчас мы очень близки к концу. Последнее, что нужно сделать, это реализовать click to restart игру на экране окончания игры. Нам нужно установить все переменные, которые мы использовали, в их начальные значения и перезапустить игру. Вот:

 ... public void mousePressed() { ... if (gameScreen==2){ restart(); } } ... void restart() { score = 0; health = maxHealth; ballX=width/4; ballY=height/5; lastAddTime = 0; walls.clear(); gameScreen = 0; }

Добавим еще несколько цветов.

Готовый Flappy Pong в полном цвете.

Вуаля! У нас есть Flappy Pong!

Полный код игры Processing можно найти здесь.

Портирование кода обработки игры в Интернет с помощью p5.js

p5.js — это библиотека JavaScript с очень похожим синтаксисом на язык программирования Processing. Это не библиотека, способная просто запускать существующий код Processing; вместо этого p5.js требует написания фактического кода JavaScript — аналогично порту JavaScript Processing, известному как Processing.js. Наша задача — преобразовать код Processing в JavaScript с помощью API p5.js. Библиотека имеет набор функций и синтаксис, похожий на Processing, и нам нужно внести определенные изменения в наш код, чтобы заставить их работать в JavaScript, но поскольку и Processing, и JavaScript имеют сходство с Java, это не так уж сложно, как кажется. . Даже если вы не являетесь разработчиком JavaScript, изменения очень тривиальны, и вы сможете легко следовать им.

Прежде всего, нам нужно создать простой index.html и добавить p5.min.js в наш заголовок. Нам также нужно создать еще один файл с именем flappy_pong.js , в котором будет размещен наш преобразованный код.

 <html> <head> <title>Flappy Pong</title> <script tyle="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.4.19/p5.min.js"></script> <script tyle="text/javascript" src="flappy_pong.js"></script> <style> canvas { box-shadow: 0 0 20px lightgray; } </style> </head> <body> </body> </html>

Наша стратегия при преобразовании кода должна состоять в том, чтобы скопировать и вставить весь наш код в flappy_pong.js а затем внести все изменения. И это то, что я сделал. И вот шаги, которые я предпринял для обновления кода:

  • Javascript — это нетипизированный язык (нет объявлений типов, таких как int и float ). Поэтому нам нужно изменить все объявления типов на var .

  • В Javascript нет void . Мы должны изменить все, чтобы function .

  • Нам нужно удалить объявления типов аргументов из сигнатур функций. (т.е. void wallMover(var index) { to function wallMover(index) { )

  • В JavaScript нет ArrayList . Но мы можем добиться того же, используя массивы JavaScript. Вносим следующие изменения:

    • ArrayList<int[]> walls = new ArrayList<int[]>(); to var walls = [];
    • walls.clear(); к walls = [];
    • walls.add(randWall); к walls.push(randWall);
    • walls.remove(index); к walls.splice(index,1);
    • walls.get(index); к walls[index]
    • walls.size() в walls.length
  • Измените объявление массива var randWall = {width, randY, wallWidth, randHeight, 0}; to var randWall = [width, randY, wallWidth, randHeight, 0];

  • Удалите все public ключевые слова.

  • Переместите все объявления color(0) в function setup() потому что color() не будет определен до вызова setup() .

  • Изменить size(500, 500); createCanvas(500, 500);

  • Переименуйте function gameScreen(){ во что-то другое, например, function gamePlayScreen(){ , потому что у нас уже есть глобальная переменная с именем gameScreen . Когда мы работали с Processing, одна была функцией, а другая — переменной типа int . Но JavaScript сбивает их с толку, поскольку они нетипизированы.

  • То же самое относится и к score() . Я переименовал его в addScore() .

Полный код JavaScript, охватывающий все в этом руководстве по обработке, можно найти здесь.

Обработка игрового кода: вы тоже можете это сделать

В этом уроке по обработке я попытался объяснить, как создать очень простую игру. Однако то, что мы сделали в этой статье, — это лишь верхушка айсберга. С языком программирования Processing можно добиться практически всего. На мой взгляд, это лучший инструмент для программирования того, что вы себе представляете. Моим фактическим намерением в этом учебнике по обработке было не обучение обработке и создание игры, а доказательство того, что программирование не так сложно. Создание собственной игры — это не просто мечта. Я хотел показать вам, что, приложив немного усилий и энтузиазма, вы можете это сделать. Я действительно надеюсь, что эти две статьи вдохновят всех попробовать программирование.