Полноценный пример

Автор: Евгений Рыжков и Татьяна Головко Дата публикации: 16.03.2011

Уголки — это настольная игра с многовековой историей, у которой существует множество вариаций. В этом примере я реализовал версию для доски 9x9. В начале игры наши фишки находится в нижнем левом углу доски и занимают область 3х3. Цель игры — заполнить в правом верхнем углу область 3х3 всеми своими фишками за минимальное количество ходов.

Передвигать свои фишки можно двумя способами:

  1. Взять фишку и переместить в соседнюю свободную клетку. Допускаются перемещения на север, юг, восток, запад, северо-запад, северо-восток, юго-запад или юго-восток относительно текущего положения. Доску переворачивать нельзя. Если фишка находится в самой левой колонке, она не может двигаться на запад, северо-запад или юго-запад. Если же находится в самой нижней строке — не может двигаться на юг, юго-восток или юго-запад.
  2. Взять фишку и перепрыгнуть через соседнюю фишку. Такой прыжок можно повторить, причем, если ты перепрыгнул через соседнюю фишку, а затем сразу есть возможность перепрыгнуть еще через одну — это засчитывается за один ход. Т.е. любое количество прыжков одной фишкой считается за один ход. Т.к. целью игры является минимальное количество ходов, игра сводится к построению цепочек фишек, которые дадут возможность остальным фишкам прыжками преодолеть максимальное расстояние.

Ниже приведен скриншот игры:

игра уголки

А вот тут можно поиграть вживую.

Так как же это работает? Рад, что ты задал этот вопрос. Я не буду приводить весь код целиком тут (его можно увидеть по адресу http://diveintohtml5.org/examples/halma.js). Я пропущу большую часть кода игры, но остановлюсь на некоторых частях, которые отвечают за рисование и за клики мышью на элементе <canvas>.

Во время загрузки страницы мы проводим инициализацию игры, устанавливая размеры <canvas> и получая его контекст.

gCanvasElement.width = kPixelWidth;
gCanvasElement.height = kPixelHeight;
gDrawingContext = gCanvasElement.getContext("2d");

Сейчас я сделаю кое-что для тебя новое — добавлю событие, которое будет отслеживать клики на холсте:

gCanvasElement.addEventListener("click", halmaOnClick, false);

Функция halmaOnClick() будет вызываться при каждом клике по холсту. Ее аргументом является объект MouseEvent, который содержит информацию, в какой именно точке был совершен клик.

function halmaOnClick(e) {
	var cell = getCursorPosition(e);
	// остальное – это посто логика процесса игры
	for (var i = 0; i < gNumPieces; i++) {
		if ((gPieces[i].row == cell.row) && (gPieces[i].column == cell.column)) {
			clickOnPiece(i);
			return;
		}
	}
	clickOnEmptyCell(cell);
}

Следующим шагом будем обработка объекта MouseEvent чтобы выяснить по какому квадрату поля был совершен клик. Доска «Уголоков» занимает всю площадь холста, поэтому любой клик приходится на какой-то из квадратов поля. Нужно просто выявить, где он произошел. Это сложно, потому что события мыши реализуются в разных браузерах по-разному.

function getCursorPosition(e) {
	var x;
	var y;
	if (e.pageX || e.pageY) {
		x = e.pageX;
		y = e.pageY;
	}
	else {
		x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
		y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
	}

Сейчас мы получили координаты места клика x и y относительно всей страницы. Но нас интересуют координаты относительно холста. Для этого поступаем следующим образом:

x -= gCanvasElement.offsetLeft;
y -= gCanvasElement.offsetTop;

Теперь у нас имеются координаты относительно холста. Например, если x=0 и y=0 мы знаем, что пользователь кликнул по верхнему левому углу холста.

Теперь можно вычислить по какому квадрату доски пользователь кликнул и затем предпринимать соответствующие действия:

var cell = new Cell(Math.floor(y/kPieceWidth), Math.floor(x/kPieceHeight));
return cell;
}

Вот так! События мыши — это непросто. Но ты можешь использовать эту же логику во всех своих canvas-приложениях. Запомни: клик мыши → документозависимые координаты → координаты относительно холста → код конкретного приложения.

Хорошо, давай теперь разберем основные типовые задачи рисования. Так как у нас графика простая, я решил при каждом изменении в игре перерисовывать полностью все изображение. Конечно, это не обязательно — контекст будет помнить все что было ранее нарисовано, даже если пользователь прокрутит страницу и холст скроется за пределами экрана или переключит вкладки браузера, а затем вернется назад. Если же ведется разработка сложной графики (например, аркадной игры), то в целях оптимизации производительности можно перерисовывать только измененные участки холста. Но это выходит за рамки данной книги. Следующий код очищает нашу доску:

gDrawingContext.clearRect(0, 0, kPixelWidth, kPixelHeight);

Рисование доски для тебя уже должно быть знакомым: оно сходно с тем как мы рисовали оси координат:

gDrawingContext.beginPath();
/* вертикальные линии */
for (var x = 0; x <= kPixelWidth; x += kPieceWidth) {
	gDrawingContext.moveTo(0.5 + x, 0);
	gDrawingContext.lineTo(0.5 + x, kPixelHeight);
}
/* горизонтальные линии */
for (var y = 0; y <= kPixelHeight; y += kPieceHeight) {
	gDrawingContext.moveTo(0, 0.5 + y);
	gDrawingContext.lineTo(kPixelWidth, 0.5 +  y);
}
/* рисуем их! */
gDrawingContext.strokeStyle = "#ccc";
gDrawingContext.stroke();

Самое интересное начинается во время отрисовки каждой фишки. Фишка представляет собой окружность. С этим мы еще не сталкивались. Кроме того, когда пользователь выбирает фишку, которую будет перемещать, мы для него ее выделим, закрасив круг. В коде аргумент p представляет фишку со свойствами row и column, которые определяют ее положение на доске. Мы используем некоторые игровые константы (столбец, строка) и переводим их в относительные координаты холста (х, у), затем рисуем окружность, а потом (если фишка выбрана), заполняем окружность цветом:

function drawPiece(p, selected) {
	var column = p.column;
	var row = p.row;
	var x = (column * kPieceWidth) + (kPieceWidth/2);
	var y = (row * kPieceHeight) + (kPieceHeight/2);
	var radius = (kPieceWidth/2) - (kPieceWidth/10);
	...

С логикой игры закончили. Теперь у нас есть координаты (x,y) относительно холста для центра окружности, которую хотим нарисовать. В canvas API нет метода специально для рисования круга, но есть метод arc(). Да и зачем специальный метод, если окружность — это замкнутая дуга. Помнишь основы геометрии? Метод arc() получает в качестве параметров координаты x и y, величину радиуса, начальный и конечный углы (в радианах) и флаг направления (false — по часовой стрелке, true — против часовой). Мы можем использовать модуль Math из Javascript для расчета радиан:

gDrawingContext.beginPath();
gDrawingContext.arc(x, y, radius, 0, Math.PI * 2, false);
gDrawingContext.closePath();

Минуточку! Ведь мы так еще ничего и не нарисовали. Методы moveTo(), lineTo(), arc() — все это «карандашные» методы. Чтобы «по-настоящему» нарисовать окружность, нужно установить значение stokeStyle() и вызвать метод stroke() чтобы сделать обводку «чернилами».

gDrawingContext.strokeStyle = "#000";
gDrawingContext.stroke();

А как быть с выбранной фигурой? Можно использовать уже созданный контур для того, чтобы нарисовать границы фигуры и затем залить ее цветом:

чтобы сделать обводку «чернилами».

if (selected) {
	gDrawingContext.fillStyle = "#000";
	gDrawingContext.fill();
}

Все, что касается рисования, мы рассмотрели. Остальная часть программы — это отслеживание верных и не верных ходов, их количества и условия окончания игры. Нам потребовалось всего девять окружностей и событие onClick чтобы создать игру на <canvas>. Ура!

Куда дальше