Pull to refresh

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

Reading time7 min
Views7.7K

День добрый. В этом топике я расскажу, как сделать 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, если вы не зарегистрированы на Хабре.
Tags:
Hubs:
+39
Comments24

Articles