Как написать ПингПонг при помощи LibCanvas


    День добрый. В этом топике я расскажу, как сделать ping-pong при помощи LibCanvas. Я значительно упростил её, оставив только самую важную часть, так как цель топика не создать игру ping-pong, а объяснить основы LibCanvas.

    Итак, в топике пошаговая инструкция, как создать ping-pong при помощи LibCanvas (без оптимизаций).

    Итак, пинг понг — две доски, от которых отбивается мячик. В общем, вы все знаете. Первое, что нам необходимо — это создать начальный html-файл. Он достаточно прост — одинокий элемент canvas, ссылки на AtomJS и LibCanvas и ссылки на файлы приложения:

    <!DOCTYPE html>
    <html>
    	<head>
    		<meta charset="utf-8" />
    		<title>LibCanvas :: ping-pong</title>
    		<link href="/styles.css" rel="stylesheet" />
    		<script src="/lib/atom.js"></script>
    		<script src="/lib/libcanvas.js"></script>
    	</head>
    	<body>
    		<canvas></canvas>
    
    		<!-- Game engine -->
    		<script src="js/init.js"></script>
    		<script src="js/controller.js"></script>
    		<script src="js/field.js"></script>
    		<script src="js/unit.js"></script>
    		<script src="js/ball.js"></script>
    	</body>
    </html>
    


    Инициализация


    Всё начинается с файла инициализации.

    В нём я вызываю LibCanvas.extract для того, чтобы была возможность использовать глобальные имена. По-умолчанию, все классы хранятся в своих пространствах имен: LibCanvas.Shapes.Circle. После extract их можно использовать сокращённо: Circle

    Вторым шагом я объявляю пространство имен для своей игрушки. Все класы будут хранится в нём.

    Последний шаг — это создание контроллера при старте dom.

    LibCanvas.extract();
    
    window.Pong = {};
    
    atom.dom(function () {
    
    	new Pong.Controller('canvas');
    });
    


    Инициализация может отличаться зависимо от приложений, но, в целом, она подобна среди них. Один из моих товарищей любит интересный подход — минимальный html-файл со всей логикой (даже создание элемента и подключение скриптов) в JavaScript. И да, этот код валиден!

    <!DOCTYPE html>
    <title>LibCanvas :: ping-pong</title>
    <script src="js/load.ls"></script>
    


    Контроллер


    Следующий шаг — создание контроллера. В нём мы создадим объект LibCanvas и игровые элементы. Все игровые классы я буду создавать при помощи atom.Class, где initialize — это конструктор.

    Pong.Controller = atom.Class({
    
    	initialize: function (canvas) {
    
    		this.libcanvas = new LibCanvas(canvas, {
    				preloadImages: { elems : 'im/elems.png' }
    			})
    			.listenKeyboard([ 'aup', 'adown', 'w', 's' ])
    			.addEvent('ready', this.start.bind(this))
    			.start();
    	},
    
    	start: function () {
    		var libcanvas = this.libcanvas;
    		[...]
    	}
    });
    


    В конструктор мы передаём объект для предзагрузки картинки. Приложение не запуститься, пока картинка не будет загружена. Это два спрайта — палки и мячика.



    Мы сообщаем LibCanvas, что мы будем использовать клавиатуру и необходимо избежать действий по-умолчанию для клавиш 'aup', 'adown', 'w' и 's'. Это позволит реализовать удобное управление и, при этом не будет, например, сдвигаться стрелочками окно браузера.

    Когда LibCanvas будет готов начать отрисовку — мы запустим метод start контроллера. К нему мы вернемся позже.

    Игровое поле


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

    Pong.Field = atom.Class({
    	Implements: [ Drawable ],
    
    	width : 800,
    	height: 500,
    
    	[...]
    });
    


    Создаем сущность и добавляем её в libcanvas для отрисовки. Обратите внимание, как мы ловко меняем размер холста. Это потому что наш объект имеет свойства width и height.

    Pong.Controller = atom.Class({
    
    	[...]
    
    	start: function () {
    		var libcanvas = this.libcanvas;
    		var field = new Pond.Field();
    		
    		libcanvas.size( field, true );
    		libcanvas.addElement( field );
    		
    		[...]
    	}
    });
    


    Мяч


    Логика мяча предельно проста — у него есть «импульс» — направление и скорость передвижения.
    Скорость задаётся в пикселях/секунду. Каждый раз во время обновления мы получаем время, которое прошло с предыдущего обновления, на которое умножем скорость. За счёт этого мы имеем постоянную скорость передвижения, независимо от fps
    Когда шарик достигает верхней или нижней границы — он ударяется и летит в другую сторону.
    appendTo позволяет легко присовить шарик полю. Нам важно знать размеры поля для начальной позиции и учёта стен.
    Отрисовка очень проста — мы просто отрисовываем нужную часть спрайта в текущий прямоугольник.
    Обратите внимание, что во время конструирования объекта свойства libcanvas ещё нету, потому необходимо дождаться события libcanvasSet и только потом орудовать с libcanvas

    Pong.Ball = atom.Class({
    	Implements: [ Drawable ],
    
    	impulse: null,
    
    	initialize: function (controller) {
    		this.impulse = new Point(
    			Number.random(325, 375),
    			Number.random(325, 375)
    		);
    
    		this.addEvent('libcanvasSet', function () {
    			this.image = this.libcanvas.getImage('elems').sprite( 23, 0, 26, 26 );
    		});
    	},
    
    	move: function (time) {
    		this.shape.move(
    			this.impulse.clone().mul(time / 1000)
    		);
    	},
    
    	update: function (time) {
    		this.move(time);
    
    		var from = this.shape.from, to = this.shape.to;
    
    		// Обрабатываем верхнюю и нижнюю границы
    		if (
    			(this.impulse.y < 0 && from.y < 0) ||
    			(this.impulse.y > 0 && to.y > this.field.height)
    		) this.impulse.y *= -1;
    	},
    
    	appendTo: function (field) {
    		this.shape = new Rectangle( 40, field.height / 2, 24, 24 );
    		this.field = field;
    		return this;
    	},
    
    	draw: function () {
    		this.libcanvas.ctx.drawImage(this.image, this.shape);
    	}
    });
    


    Добавляем его вызов в контроллер. Нам необходимо каждый кадр обновлять положение шарика, потому мы подписываемся на обновление при помощи addFunc
    Pong.Controller = atom.Class({
    
    	[...]
    
    	start: function () {
    		[...]
    		    ball  = new Pong.Ball();
    
    		libcanvas
    			[...]
    			.addElement( ball.appendTo( field ) )
    			.addFunc(function (time) {
    				ball.update( time );
    				libcanvas.update();
    			});
    	}
    });
    


    Создаём юнитов


    Следующее, что нам требуется — это ракетки. Они будут управляться при помощи клавиатуры (w-s для левой и вверх-вниз для правой).
    Этот класс будет отвечать за контролы, передвижение, соприкосновение с шариком.
    Обратите внимание, что свойство «speed» — статическое, то есть добавлено в прототип. Мы его не будем изменять, а только использовать.
    В controls мы привязываемся к обновлению холста и проверяем состояние необходимых клавиш. При необходимости — сдвигаем объект.
    Интересный способ передвинуть фигуру на нужную скорость — мы просто используем метод move нашего прямоугольника для этого.
    fitToField убеждается, что элемент находится в допустимых пределах и, если это не так, то возвращает его на место.
    В методе draw, по аналогии с Ball, отрисовывается нужная часть картинки в текущую shape.

    Pong.Unit = atom.Class({
    	Implements: [ Drawable ],
    
    	size: { width: 20, height: 100, padding: 20 },
    	speed: new Point( 0, 300 ),
    	score: 0,
    
    	controls: function (up, down) {
    		this.addEvent('libcanvasSet', function () {
    			var lc = this.libcanvas.addFunc(function (time) {
    				if (lc.getKey(up)) {
    					this.move( -time );
    				} else if (lc.getKey(down)) {
    					this.move(  time );
    				}
    			}.bind(this));
    		});
    		return this;
    	},
    
    	appendTo: function (field, number) {
    		var s = this.size;
    
    		this.field  = field;
    		this.number = number;
    		this.shape = new Rectangle({ // field.width, field.height
    			from: [
    				(number == 2 ? field.width - s.width - s.padding : s.padding),
    				(field.height - s.height) / 2
    			],
    			size: s
    		});
    		return this;
    	},
    
    	fitToField: function () {
    		var shape = this.shape;
    
    		var top = shape.from.y, bottom = shape.to.y - this.field.height;
    
    		if (top    < 0) shape.move(new Point(0, -top));
    		if (bottom > 0) shape.move(new Point(0, -bottom));
    	},
    
    	move: function (time) {
    		this.shape.move( this.speed.clone().mul( time / 1000 ) );
    
    		this.fitToField();
    	},
    
    	draw: function() {
    		this.libcanvas.ctx.drawImage(
    			this.libcanvas.getImage('elems').sprite(0,0,20,100),
    			this.shape
    		);
    	}
    });
    


    Юнитов будем создавать на поле, где будем задавать им управление и положение:
    Pong.Field = atom.Class({
    	[...]
    	createUnits: function (libcanvas) {
    
    		this.unit = new Pong.Unit()
    			.controls('w', 's')
    			.appendTo( this, 1  );
    
    		this.enemy = new Pong.Unit()
    			.controls('aup', 'adown')
    			.appendTo( this, 2 );
    
    		libcanvas
    			.addElement( this.unit  )
    			.addElement( this.enemy );
    	},
    	[...]
    


    Естественно, необходимо добавить вызов метода в Контроллер:

    Pong.Controller = atom.Class({
    	[...],
    
    	start: function () {
    		[...],
    		field.createUnits( libcanvas );
    	}
    });
    


    Взаимодействие объектов


    Теперь необходимо заставить шарик взаимодействовать с крайними границами и игроками. Добавляем простой метод в Ball.
    Обратите внимание, что необходимо проверять направление движения шарика, иначе он может «застревать» в игроках и стенах.

    Pong.Ball = atom.Class({
    	[...]
    
    	checkCollisions: function () {
    		var coll  = this.field.collidesUnits( this ),
    		    isOut = this.field.isOut( this.shape );
    
    		if (
    			(( coll < 0 || isOut < 0 ) && this.impulse.x < 0) ||
    			(( coll > 0 || isOut > 0 ) && this.impulse.x > 0)
    		) this.impulse.x *= -1;
    	},
    
    	update: function (time) {
    		[...]
    		this.checkCollisions();
    	},
    
    	[...]
    });
    


    Проверка внутри Field очень простая.
    Проверку на столкновение с юнитом мы перекладываем на плечи юнита, и только возвращаем направление.
    Проверка на выход за правую или левую границу — тоже очень простая. В случае соприкосновения с границей мы увеличиваем счёт противоположного игрока.

    Pong.Field = atom.Class({
    	[...]
    	collidesUnits: function (ball) {
    		return this.unit .collides(ball) ? -1 :
    		       this.enemy.collides(ball) ?  1 : 0;
    	},
    
    	isOut: function (shape) {
    		if (shape.from.x < 0) {
    			this.enemy.score++;
    			return -1;
    		} else if (shape.to.x > this.width) {
    			this.unit.score++;
    			return 1;
    		}
    		return 0;
    	},
    	[...]
    


    Внутри Unit мы воспользуемся встроенным методом Rectangle().intersect, для проверки пересечения двух прямоугольников.
    Pong.Unit = atom.Class({
    	[...]
    	collides: function (ball) {
    		return ball.shape.intersect(this.shape);
    	},
    });
    


    Вывод счета


    Последний шаг — отобразить счёт игроков. Это можно легко сделать при помощи ctx.text — он позволяет вывоодить текст более прибилиженно к css, указывать отступы, прямоугольник, в который необходимо вывести текст и некоторые дополнительные возможности.

    Pong.Field = atom.Class({
    	[...]
    	drawScore: function (unit, align) {
    		this.libcanvas.ctx.text({
    			text: unit.score,
    			size: 32,
    			padding: [0, 70],
    			color: 'white',
    			align: align
    		});
    		return this;
    	},
    
    	draw: function () {
    		this
    			.drawScore( this.unit , 'left'  )
    			.drawScore( this.enemy, 'right' );
    	}
    });
    


    Заключение


    Вот и всё. Полный код и игру вы можете найти по адресу

    libcanvas.github.com/games/pingpong/



    Игру из топика можно развивать дальше. Например, добавить сетевую игру с сервером на node.js и WebSocket.
    Или добавить красивый внешний вид, анимации. Также можно усовершенстовать геймплей — добавить преграды, иной угол отражения шарика.
    Это всё делается очень легко при помощи LibCanvas. Какие темы вас интересуют? Если будут желающие — я их опишу.

    Ещё вопрос — стоит ли описывать более базовые вещи, делать топики про LibCanvas не так загруженные информацией, а более узкие и описывающие отдельные мелкие аспекты или подобные полноформатные статьи воспринимаются достаточно легко?

    Мне кажется, что иногда мысли были довольно сумбурны, потому не стесняйтесь задавать вопросы в комментах или на емейл shocksilien@gmail.com, если вы не зарегистрированы на Хабре.
    Поделиться публикацией

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 24

      +6
      Неделя Canvas на хабре, это гуд.

      Можно браться за танчики :)


        +4
        Кстати.
        А как лучше всего в данном случае отрисовывать землю?
          0
          Можно на отдельном слое:
          libcanvas.createLayer( 'ground' )
          

          Если хотите заняться этим, то могу консультировать и помочь.
            0
            Окей, если появятся вопросы — то обращусь
              0
              Вопрос не в слое :)
              Там же тысячи взаимодействующих объектов — клеточным автоматом что ли обрабатывать? :)
                0
                Таки объясните?) Я именно в эту реализацию не играл, но не вижу проблем)
                  0
                  Имею в виду вообще большие количества маленьких точек, подчиняющихся физическим законам. Разве моделирование их поведения не требует каких-то ёмких вычислений?
                    0
                    Ну, вроде, там особо сложных вычислений нету. В видео
                    Scorched Earth — Game Play песчинки падают по принципу «под мной нету песчинки — я должна опуститься на пиксель вниз». Это хорошо видно в 1:15. Берем все пиксели в радиусе взрыва и пересчитываем их. В современных флешках вообще векторная земля.
            0
            подскажете как игра называется? по скрину вспомнил, что играл давным-давно на спектруме, может где-то на эмуляторе найду =)
              0
              Scorched Earth
                0
                Судя по скрину — идёт под DosBox ;)
              +1
              Какие темы вас интересуют? Если будут желающие — я их опишу.

              Интересует возможность создания «серьезных» игр — т.е. не уровня Pong'а, а уровня по крайней мере старых DOS'овских игрушек — вроде тех же Space Quest, Kyrandia, и Dune 2.
              И насколько я понимаю в возможностях создания полноценных MMOG Канвас пока проигрывает тем же HTML5+CSS3 (см. Aves Engine)?
                +1
                Осторожнее, можно нарваться на холивар
                  0
                  Извините, я не специально. :)
                  Мне действительно интересно. Пока сложилось ощущение, что Canvas для таких вещей не очень подходит — но при этом быстро догоняет Флэш. Поэтому и хотел найти опровержение или подтверждение своим мыслям.
                  0
                  Это холиварный вопрос. Но, имхо, не проигрывают. Aves Engine просто не умеют готовить Canvas.
                  +3
                  Да. Хочется более серьезных туториалов. Может не таких серьезных как Dune 2, но вот про создание Lunar Lander я бы почитал с удовольствием.
                    0
                    Ну Lunar Lander не намного серьёзнее пинг-понга)
                      0
                      Не сказал бы. По крайней мере присутствуют операции с векторами и примитивная физика. А в понге (в этом конкретно) нет ничего кроме тупо смены направления движения шарика при соударении с битой или со стеной.
                        0
                        Там чем-то похоже на Asteroids.
                        Особенность в том, что impulse меняется зависимо от направления и гравитации. Спасибо за отзыв, я попробую написать)
                        пс. Можете попробовать сделать Вы, а я — помогу ;)
                          0
                          Я совсем не знаю Libcanvas. И у меня плохо с векторным анализом. Потому и клянчу туториал :)

                          На чистом яваскрипте могу попробовать написать что-то, но я совсем не представляю как определить расстояние от объекта (посадочного модуля) до поверхности.
                            0
                            Будет возможность выучить)
                          +1
                          Просчёт столкновения с полем сделать очень легко. Это ведь всего-лишь набор прямых. Мы их храним в массиве:
                          var lines = [
                            new Line(10, 10, 20, 20),
                            ...
                            new Line(710, 10, 720, 20)
                          ];
                          


                          А потом, кажде обновление кадра, проверяем расстояние корабля до линии
                          Ship = atom.Class({
                          	[...]
                          	collision: function (lines) {
                          		for (var i = lines.length; i--;) {
                          			if (lines.distanceTo( this.position ) < this.radius) {
                          				return true;
                          			}
                          		}
                          		return false;
                          	}
                          });
                          


                          distanceTo относительно требовательна к ресурсам, потому можно оптимизировать за счёт того, что предварительно проверять, находится ли корабль достаточно низко:
                          Ship = atom.Class({
                          	[...]
                          	collision: function (lines) {
                          		if ( this.position.y > this.minCollisionHeight ) {
                          			return false;
                          		}
                          
                          		for (var i = lines.length; i--;) {
                          		[..]
                          	}
                          });
                          


                          Естественно, minCollisionHeight вычисляется один раз перед началом приложения. Достаточно пройти все отрезки, найти самый высокий Y им приплюсовать к ней радиус корабля:

                          Ship = atom.Class({
                          	[...]
                          	genMinCollisionHeight: function (lines) {
                          		var max = 0;
                          		for (var i = lines.length; i--;) {
                          			max = Math.max( max, lines[i].y );
                          		}
                          		return max + this.radius;
                          	}
                          });
                          


                          Это, естественно, если использовать LibCanvas
                      0
                      Черт, я подсел играть в понг сам с собой:)

                      Заметил, что даже несмотря на высокий ФПС (около 60) быстродвижущиеся объекты на канвасе (в вашем случае шарик) двигаются как-то малость дергано и неравномерно. Я, правда, ковырял библиотеку cake.js, думал может это она тормознутая, но у вас вот то же самое с шариком.
                      • НЛО прилетело и опубликовало эту надпись здесь

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

                        Самое читаемое