Пишем пингпонг на JavaScript
Эта статья будет полезна в первую очередь новичкам, изучающим JavaScript. Просматривая уроки по JavaScript’у я понял, что нормального материала достаточно мало, много англоязычного и частично некоторая информация устарела и так, как показывают в некоторых уроках, я бы категорически не рекомендовал писать и учиться этому. Поэтому в этой статье я предлагаю написать вместе со мной простейший пингпонг и немного поговорить о JavaScript’e мы не будем сильно заморачиваться с кроссбраузерностью и откажемся сейчас от всяких там старых ие и индивидуальных багов разных версий браузеров. Писать мы будем в стиле ES5 хотя и для сегодняшнего дня 18 декабря 2015 года, уже могу рекомендовать начинать пользоваться ES6, но до широкого продакшена я пока в 2015 году у ES2015 я не вижу перспектив.Что из себя представляет игра?
С точки зрения изучения JavaScript’a на мой взгляд гораздо интереснее на нем пробовать писать нечто большее чем нужно в повседневной жизни, типа модальных окошек, попапов, менюшек и других интерфейсных штук. Мы будем писать простейший пинг понг с использованием возможностей HTML5. В нашей игре будут некие объекты которые будут взаимодействовать между собой, а именно шарик, ракетки (2шт), игроки(2шт). Правила простые, счет до 10, гол засчитывается после пропуска мячика за линию ракетки.
Опишем наши игровые объекты
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
//Опишем наши игровые объекты + научим их рисовать себя на канвасе и передвигаться var Ball = function () { return { radius: 8, color: '#FFCC00', x: 0, y: 0, yspeed: 5, xspeed: 7, bounce: 1.1, //коофицент упругости - для ускорения шарика после отскока render: function (ctx) { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, 2*Math.PI); ctx.fillStyle = this.color; ctx.fill(); }, //Передвижение шара всегда происходит с определенной скоростью //по этому мы не будем передавть x y для кастомного перемещения. move: function () { this.x = this.x + this.xspeed; this.y = this.y + this.yspeed; } } }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//Ракетка var Bracket = function () { return { w: 10, h: 100, x: 0, y: 0, speed: 20, color: '#CCFF00', render: function (ctx) { ctx.fillStyle = this.color; ctx.fillRect(this.x, this.y, this.w, this.h); } } }; |
Игрок
1 2 3 4 5 6 7 8 |
[cce lang="javascript"] //Собственно сам игрок с его свойствами var Player = function () { return { rate: 0 }; }; |
Теперь займемся описанием самой игры. Это будет игровой объект, который при инициализации сохраняет ссылку на canvas в котором мы будем рисовать и подпишется на необходимые события с клавиатуры, которыми мы будем управлять. Игровые методы мы опишем в прототипе объекта.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//Теперь сама игра var Game = function () { //Сохраним ссылку на контекст //для дальнейшей передачи в ивенты var _this = this; //Параметры с которыми будет игра this.params = { width: 960, height: 600, state: 'loading', //Состояние игры maxRate: 10 //до скольки будет идти матч. }; //Сохраняем ссылки на canvas и контекст для дальнейшего рисования this.canvasBlock = document.getElementById('pingpong'); this.ctx = this.canvasBlock.getContext('2d'); //Подписываемся на события кнопок document.addEventListener('keydown', function (event) { _this.keyDownEvent.call(_this, event); }); return this; }; |
Теперь разберем игровую логику более подробно. Мы будем использовать requestAnimationFrame в котором будем запускать игровой loop который будет равен 1ой итерации логики нашей игры. Саму логику мы разделим на несколько частей. Физика — расчет перемещения объектов на нашем холсте. Игровая логика — проверка на пропуск гола, расчет очков, определение победителя и т.п. Рисование — непосредственно сам рендер нашего состояния игры в канвас. Также опишем методы для старта игры, рестарта игры, рестарта шарика и обработку событий нажатия клавиш игроками. Весь код прокомментировал, надеюсь будет понятно.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 |
Game.prototype = { //Старт игры startGame: function () { var _this = this; //Инициализируем игровые объекты this.objects = { ball: new Ball(), player1: new Player(), player2: new Player(), bracket1: new Bracket(), bracket2: new Bracket() }; //Меняем состояние this.params.state = 'game'; //Расставляем стартовые позиции ракеток this.objects.bracket1.x = 50; this.objects.bracket1.y = this.params.height / 2 - this.objects.bracket1.h / 2; this.objects.bracket2.x = this.params.width - 50; this.objects.bracket2.y = this.params.height / 2 - this.objects.bracket1.h / 2; //Перекрасим второго игрока this.objects.bracket2.color = '#00FFCC'; //Запускаем игровой цикл this.loop(); }, //Игровой цикл loop: function () { var _this = this; //Логика игры this.logic(); //Физика игры this.physic(); //Рендер игры this.render(); //Используем замыкание для передачи контекста this.requestLoop = requestAnimationFrame(function(){ _this.loop.call(_this); }); }, //Логика игры logic: function () { //Для краткости записи var ball = game.objects.ball; //Если сейчас идет игра if(this.params.state == 'game') { //И шарик оказался за первым игроком if (ball.x + ball.radius/2 < 0) { //Засчтитаем гол this.objects.player2.rate++; //Сменим состояние игры this.params.state = 'playerwait'; //Сохарним информацию о забившем this.params.lastGoalBracket = this.objects.bracket2; this.params.lastGoalPlayer = 'player2'; } //Шарик оказался за выторым игроком if (ball.x + ball.radius/2 > game.params.width) { //Засчтитаем гол this.objects.player1.rate++; //Сменим состояние игры this.params.state = 'playerwait'; //Сохарним информацию о забившем this.params.lastGoalBracket = this.objects.bracket1; this.params.lastGoalPlayer = 'player1'; } //Проверяем наличие победителя //Если кто-то из игроков набрал необходимое количество очков //Он выиграл if(this.objects.player1.rate === this.params.maxRate) { alert('1 игрок выиграл'); this.gameRestart(); } if(this.objects.player2.rate === this.params.maxRate) { alert('2 игрок выиграл'); this.gameRestart(); } } }, //Физика игры physic: function () { //Для краткости записи var ball = game.objects.ball, b1 = game.objects.bracket1, b2 = game.objects.bracket2; //Передвигаем шар game.objects.ball.move(); //Отскок слева if (ball.x + ball.radius/2 < 0) { game.objects.ball.xspeed = -game.objects.ball.xspeed; } //Отскок Справа if (ball.x + ball.radius/2 > game.params.width) { game.objects.ball.xspeed = -game.objects.ball.xspeed; } //Отскок от границ canvas по высоте if (ball.y + ball.radius/2 > game.params.height || ball.y + ball.radius/2 < 0) { game.objects.ball.yspeed = -game.objects.ball.yspeed; } //Отскок шарика от 1 блока if(ball.x <= 60 && ball.y >= b1.y && ball.y <= b1.y+b1.h) { ball.xspeed = -ball.xspeed; //Ускоряем шарик ball.xspeed = ball.xspeed * ball.bounce; } //Отскок шарика от 2 блока if(ball.x >= this.params.width-50 && ball.y >= b2.y && ball.y <= b2.y+b2.h) { ball.xspeed = -ball.xspeed; //Ускоряем шарик ball.xspeed = ball.xspeed * ball.bounce; } //В состоянии ожидания пуска шарика от ракетки игрока, выставляем шарик рядом с ракеткой забившего игрока. if(this.params.state === 'playerwait') { ball.xspeed = 0; ball.yspeed = 0; if(this.params.lastGoalPlayer === 'player1') { ball.x = this.params.lastGoalBracket.x + this.params.lastGoalBracket.w + ball.radius + 1; ball.y = this.params.lastGoalBracket.y + this.params.lastGoalBracket.h/2; } if(this.params.lastGoalPlayer === 'player2') { ball.x = this.params.lastGoalBracket.x - ball.radius - 1; ball.y = this.params.lastGoalBracket.y + this.params.lastGoalBracket.h/2; } } //Не позволяем вылезать блокам за canvas и возврщаем их на место if(b1.y <= 0) b1.y = 1; if(b2.y <= 0) b2.y = 1; if(b1.y+b1.h >= this.params.height) b1.y = this.params.height-b1.h; if(b2.y+b2.h >= this.params.height) b2.y = this.params.height-b2.h; }, //Рендер игры render: function () { //Чистим канвас на каждом кадре game.ctx.fillStyle = '#eeeeee'; game.ctx.fillRect(0,0, game.params.width, game.params.height); //Рендерим шарик game.objects.ball.render(game.ctx); game.objects.bracket1.render(game.ctx); game.objects.bracket2.render(game.ctx); game.renderRate(game.ctx); }, //Показываем счет игры renderRate: function (ctx) { var rateText = game.objects.player1.rate + ' : ' + game.objects.player2.rate; ctx.fillStyle = '#000000'; ctx.font = "20px Arial"; ctx.fillText(rateText,game.params.width/2,50); }, //Инициализация игровых событий keyDownEvent: function (event) { var kCode = event.keyCode; //1-вверх if(kCode === 49) { game.objects.bracket1.y = game.objects.bracket1.y + game.objects.bracket1.speed; } //2-вниз if(kCode === 50) { game.objects.bracket1.y = game.objects.bracket1.y - game.objects.bracket1.speed; } //9-вверх if(kCode === 57) { game.objects.bracket2.y = game.objects.bracket2.y + game.objects.bracket2.speed; } //0-вниз if(kCode === 48) { game.objects.bracket2.y = game.objects.bracket2.y - game.objects.bracket2.speed; } //E - рестарт шарика if(kCode === 69) { this.restartBall(); } //R - рестарт игры if(kCode === 82) { this.restartGame(); } //Пробел - пуск шарика if(kCode === 32 && game.params.state === 'playerwait') { this.kickBall(); } }, //Пуск шарика после гола kickBall: function () { this.objects.ball.xspeed = 3; this.objects.ball.yspeed = 3; this.params.state = 'game'; }, //Стоп игра stopGame: function () { //Обновляем состояние this.params.state = 'stop'; //Останавливаем цикл cancelAnimationFrame(this.requestLoop); //Убираем слушателей событий document.removeEventListener('keydown', this.keyDownEvent); //Чистим игровые объекты delete(this.objects); }, pauseGame: function () { this.state = 'pause'; }, //Рестарт шарика restartBall: function () { this.objects.ball.x = game.params.width/2; this.objects.ball.y = game.params.height/2; this.objects.ball.xspeed = 3; this.objects.ball.yspeed = 3; }, //Рестарт игры restartGame: function () { this.stopGame(); this.startGame(); } }; |
После всех проведенных манипуляций остается запустить игру после загрузки html)
1 2 3 4 5 |
//При загрузке window, стартуем нашу игру window.onload = function () { window.game = new Game(); game.startGame(); } |
Сама по себе организация кода и логика будет подходить под огромное количество игр, в первую очередь я хотел продемонстрировать подход к организации игровой логики, цикла, работы с объектами, все сделано осознанно на достаточно примитивном уровне, для легкости понимания новичками.
Поиграть в пинпонг можно тут, посмотреть весь код можно на гитхабе.
Спасибо, за внимание.
Комментарии
15.06.2016 в 11:27
На этом алгоритме построил пинг понг в 3D 😀
19.06.2016 в 21:21
Прикольно получилось, только у меня дико тормозит)