Пишем пингпонг на JavaScript
Эта статья будет полезна в первую очередь новичкам, изучающим JavaScript. Просматривая уроки по JavaScript’у я понял, что нормального материала достаточно мало, много англоязычного и частично некоторая информация устарела и так, как показывают в некоторых уроках, я бы категорически не рекомендовал писать и учиться этому. Поэтому в этой статье я предлагаю написать вместе со мной простейший пингпонг и немного поговорить о JavaScript’e мы не будем сильно заморачиваться с кроссбраузерностью и откажемся сейчас от всяких там старых ие и индивидуальных багов разных версий браузеров. Писать мы будем в стиле ES5 хотя и для сегодняшнего дня 18 декабря 2015 года, уже могу рекомендовать начинать пользоваться ES6, но до широкого продакшена я пока в 2015 году у ES2015 я не вижу перспектив.Что из себя представляет игра?
С точки зрения изучения JavaScript’a на мой взгляд гораздо интереснее на нем пробовать писать нечто большее чем нужно в повседневной жизни, типа модальных окошек, попапов, менюшек и других интерфейсных штук. Мы будем писать простейший пинг понг с использованием возможностей HTML5. В нашей игре будут некие объекты которые будут взаимодействовать между собой, а именно шарик, ракетки (2шт), игроки(2шт). Правила простые, счет до 10, гол засчитывается после пропуска мячика за линию ракетки.
Опишем наши игровые объекты
//Опишем наши игровые объекты + научим их рисовать себя на канвасе и передвигаться 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; } } };
//Ракетка 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); } } };
Игрок
[cce lang="javascript"] //Собственно сам игрок с его свойствами var Player = function () { return { rate: 0 }; };
Теперь займемся описанием самой игры. Это будет игровой объект, который при инициализации сохраняет ссылку на canvas в котором мы будем рисовать и подпишется на необходимые события с клавиатуры, которыми мы будем управлять. Игровые методы мы опишем в прототипе объекта.
//Теперь сама игра 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ой итерации логики нашей игры. Саму логику мы разделим на несколько частей. Физика — расчет перемещения объектов на нашем холсте. Игровая логика — проверка на пропуск гола, расчет очков, определение победителя и т.п. Рисование — непосредственно сам рендер нашего состояния игры в канвас. Также опишем методы для старта игры, рестарта игры, рестарта шарика и обработку событий нажатия клавиш игроками. Весь код прокомментировал, надеюсь будет понятно.
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)
//При загрузке window, стартуем нашу игру window.onload = function () { window.game = new Game(); game.startGame(); }
Сама по себе организация кода и логика будет подходить под огромное количество игр, в первую очередь я хотел продемонстрировать подход к организации игровой логики, цикла, работы с объектами, все сделано осознанно на достаточно примитивном уровне, для легкости понимания новичками.
Поиграть в пинпонг можно тут, посмотреть весь код можно на гитхабе.
Спасибо, за внимание.
Последняя редакция 19 июня, 2016 в 05:06