Pull to refresh

Крошечный Tron на JS (30 строк кода)

Reading time 7 min
Views 7.7K
image

Собственно, продолжая тренд недели.

Ещё первый раз прочитав крошечный excel захотелось написать что либо подобное — маленькое и прикольное. Увидев змейку — понял, что стоит написать игру. Прочитав коммент — «хочу пакмана с фирменными звуками» решил что буду писать «сайтмана», на canvas, с web audio api (и вака-вака-вака) и пожиранием страниц.

Но этому было не суждено сбыться, подробнее — под катом.

Для тех кому не интересно это всё читать: управление — стрелками, запускать можно на любой странице:
jsfiddle, source.
исходный код
siteman = {
    'settings':{'size' : 10, 'speed' : 5, 'time':new Date().getTime(), 'width':window.innerWidth, 'height':window.innerHeight},
    'coord':{'x' : 0 + 20, 'y' : 0 + 20}, // plus siteman size
    'direction':{
        'current':{'x': 0.75 * Math.PI, 'y' : 1.75 * Math.PI, 'clock' : false, 'calc_x' : function(){ return siteman.coord.x +  siteman.settings.speed}, 'calc_y' : function(){return siteman.coord.y}},
        37:{'x': 0.75 * Math.PI, 'y' : 1.75 * Math.PI, 'clock' : true, 'calc_x' : function(){ return siteman.coord.x -  siteman.settings.speed}, 'calc_y' : function(){return siteman.coord.y}},
        38:{'x': 1.75 * Math.PI, 'y' : 0.75 * Math.PI, 'clock' : false, 'calc_x' : function(){ return siteman.coord.x}, 'calc_y' : function(){return siteman.coord.y -  siteman.settings.speed}},
        39:{'x': 0.75 * Math.PI, 'y' : 1.75 * Math.PI, 'clock' : false, 'calc_x' : function(){ return siteman.coord.x +  siteman.settings.speed}, 'calc_y' : function(){return siteman.coord.y}},
        40:{'x': 1.75 * Math.PI, 'y' : 0.75 * Math.PI, 'clock' : true, 'calc_x' : function(){ return siteman.coord.x}, 'calc_y' : function(){return siteman.coord.y +  siteman.settings.speed}}
    },
    'ctx':document.body.appendChild((function(element){element.width=window.innerWidth; element.height=window.innerHeight; element.style.cssText='background:rbga(60, 40, 20, 1); position:absolute; top:0; z-index: 100000000;'; return element;})(document.createElement('canvas'))).getContext("2d"),
    'h_ctx':new function(){this.chain = function(method, args){siteman.ctx[method].apply(siteman.ctx, args); return this;}},
    'checkCollision':function(data){return data[0] == 128 && data[1] == 128 && data[2] == 128;}
};
siteman.ctx.fillStyle = "rgb(128, 128, 128)";
siteman.h_ctx.chain('beginPath').chain('fillRect', [0, 0, window.innerWidth, window.innerHeight]);
(function game_loop(){
    siteman.coord = {'x':siteman.direction.current.calc_x(), 'y':siteman.direction.current.calc_y()};
    siteman.ctx.fillStyle = "rgb(255, 255, 0)";
    siteman.h_ctx.chain('beginPath').chain('arc', [siteman.coord.x, siteman.coord.y,  siteman.settings.size, 0.25 * Math.PI, 1.25 * Math.PI, siteman.direction.current.clock]).chain('fill')
    siteman.h_ctx.chain('beginPath').chain('arc', [siteman.coord.x, siteman.coord.y,  siteman.settings.size, siteman.direction.current.x, siteman.direction.current.y, siteman.direction.current.clock]).chain('fill');
    var offset_x = siteman.settings.size * (siteman.coord.x != siteman.direction.current.calc_x() ? (siteman.direction.current.clock ? -1 : 1) : 0),
        offset_y = siteman.settings.size * (siteman.coord.y != siteman.direction.current.calc_y() ? (siteman.direction.current.clock ? 1 : -1) : 0);
    if(!siteman.checkCollision(siteman.ctx.getImageData(siteman.coord.x + offset_x, siteman.coord.y +  offset_y,1,1).data)){
        alert('Game over. Your score is: '+(new Date().getTime() - siteman.settings.time)+'. Siteman size: '+siteman.settings.size+' and speed:'+siteman.settings.speed+', map width: '+siteman.settings.width+' and height: '+siteman.settings.height);
    }else{
		setTimeout(function(){game_loop();}, 1000 / 60); //requestAnimationFrame(game_loop);
    }
})();
document.addEventListener('keydown', function (e) {siteman.direction.current=siteman.direction[e.keyCode] || siteman.direction.current}, false);



С чего всё началось?


Ещё задолго до «марафона 30-ти строчных приложений на js» хотел написать свою мини-игру — новые технологии освоить и «приобщиться к гейм-деву». Но, поняв что получившийся код полное говноплохо пахнет и нужно всё переписать — как то потерял энтузиазм продолжать, хотя навыки с работы с канвасом остались. Да и надежд не оставляю когда-нибудь продолжить.

А вот теперь вот подвернулся случай всё же сделать что-нибудь, маленькое и интересное. Выбор пал на пакмана который пожирает сайт оставляя после себя черноту, но по итогу получилась тренировочная комната tron для одного игрока.

О чём это всё?


О небольшой куче математики(первый раз над постройкой отрезка нужной длины на canvas я потратил 3 дня), обсчёте коллизий, рисование фигур нужной формы и прочих приёмов обычного и ненормального программирования. Вкратце, т.к большинство кода является тривиальными вещами.

Согласен, некоторые строки _слишком_ длинные, например строка с созданием canvas(11). Но, считайте что это абстракция, типа «нарисовать сайтмана», «построить канвас», «вывести результат». Так же можно было бы уменьшить длину некоторых переменных, но мы ведь не в JS1K играем, правда?

Чем этот код интересен?


0) Логикой:
она проста — инициализируем siteman'а, определяем его свойства, рисуем канвас поверх страницы, биндимся на нажатие всех клавиш и «слушаем» только определённые а так же отображем siteman'а, обсчитываем столкновения в основном цикле.

1) Реализацией chaining:

// как выглядит в source
'h_ctx':new function(){this.chain = function(method, args){siteman.ctx[method].apply(siteman.ctx, args); return this;}}
/*
  сохраняем локальный контекст у this с помощью вызова функции-конструктора и создания объекта-в-себе, 
иначе this у функции будет равен window, и никаких цепных вызовов не получилось бы.
 А так, позвращая this мы сможем опять использовать все методы, доступные объекту h_ctx

и ещё одна интересная фишка, позволяющая вызывать нужный метод с нужными аргументами - siteman.ctx[method].apply(siteman.ctx, args)
собственно первая часть это "где искать метод", .apply - принимает то, что передать как this и массив аргументов.
вместо "короткой" записи 
*/
siteman.h_ctx.chain('beginPath').chain('arc'...
/*
пришлось бы писать
*/
siteman.ctx.beginPath();
siteman.ctx.arc(...
/*
и в 30 строк мы бы ну никак не влезли бы
*/


2) Рисуноком siteman'а. Он состоит из двух заполненных перпендикулярных полу-кругов (спасибо этой статье за наводку, причём один из них «статичен» (изменяется только порядок заполнения — по или против часовой стрелки), другой меняет координаты при смене направления движения (вверх+вниз — влево+вправо), собственно таким вот образом получилось довольно компактно вместить рисунок пакмана который смотрит во все стороны.

Состояние движения я сначала хотел расписать по нормальному, потом понял что это занимает слишком много места, потому записал все необходимые формулы прямо в объект, содержащий направление движения. В итоге имеем то, что имеем

/*зарисовать либо нижний-левый полукруг либо верхний-правый*/
siteman.h_ctx.chain('beginPath')
.chain('arc', [siteman.coord.x, siteman.coord.y,  siteman.settings.size, 0.25 * Math.PI, 1.25 * Math.PI, siteman.direction.current.clock])
.chain('fill')
/*
и дорисовать в нужной четверти второй полукруг 
*/
siteman.h_ctx.chain('beginPath')
.chain('arc', [siteman.coord.x, siteman.coord.y,  siteman.settings.size, siteman.direction.current.x, siteman.direction.current.y, siteman.direction.current.clock])
.chain('fill');


3) Исчислением коллизиий:

/*
т.к я "опустил" индикатор направления - теперь приходится "извращаться" что бы узнать куда, к чёрту, смотрит siteman
и таким уже образом высчитывать координаты пикселя перед ним
самая сложная для меня часть, я ни разу не "классический" программист, да и в школе с педагогом по математике у меня не сложилось
приходится навёрстывать в ускоренном режиме
*/
var offset_x = siteman.settings.size * (siteman.coord.x != siteman.direction.current.calc_x() ? (siteman.direction.current.clock ? -1 : 1) : 0),
        offset_y = siteman.settings.size * (siteman.coord.y != siteman.direction.current.calc_y() ? (siteman.direction.current.clock ? 1 : -1) : 0);
/*
и собственно функциия, тупо сравнивающая первые три пришедшие ей точки на равность 128(серый цвет, основа для канваса). Изначально хотел сделать перебор всех точек впереди, но не влезло по-нормальному. Потому проверка только на одну точку, но вроде нормально отрабатывает
*/
    if(!siteman.checkCollision(siteman.ctx.getImageData(siteman.coord.x + offset_x, siteman.coord.y +  offset_y,1,1).data))

Как работает и почему именно такие формулы? Если координата по x или y изменится — значит нужно учитывать смещение по этой координате(кстати, так же можно добавить композитные движения), иначе не учитывать. По тому как заполнять круг(по или против часовой стрелки) понимаем направление движения, и либо отнимаем, либо добавляем смещение на размер.
В конце прибавляем то что у нас получилось к координатам, потому что нас интересует то, что будет перед нами(отнимаем — после).
Формулы исчисления координат очень схожи с теми, что я изначально использовал для построения шлейфа.

4) Ну и немного самим созданием canvas, но это уже изврат, так что и объяснять я его не буду, ни разу не хорошая практика.

Код работает как от requestAnimationFrame так и от setTimeout. Почему по дефолту стоит setTimeout? Потому что на linux я не смог запустить «полифил» в одну строку, так что так стабильнее.

Чем этот код плох?


1) Нельзя клонировать siteman'а. Если бы было можно — 100% можно было бы запихнуть небольшую фабрику siteman'ов и были бы хоть какие-то противники. Сейчас я это вижу, но не хочу переписывать всё в третий раз.
2) Немного скучно просто гонять по пустой странице. Получилась такая вот быстро жиреющая змейка, чем tron.
3) Некоторые строки имеют слишком большую (во всех смыслах) абстракцию.
4) Всё же правильнее было бы считать через дельту от прошедшего времени и только тогда двигать siteman'а, когда он «может».
5) Ну и немного странно выглядит картинка на поворотах, но тут уж придётся рисовать не пакмана а целый круг, или извращаться с координатами ещё больше.
6) Да, можно съесть самого себя. если нажать противоположную движению клавишу. Т.к направление движения нигде явно не указано — получилась бы тяжёлая строка с биндами клавиш, но это вполне реально сделать.
7) Не хватает reset'а

Да, и я долго пытался заставить отображаться нормальный шлейф, но если учитывать направление (а не делать «простой» шлейф который будет всегда-за-пакманом) то получается слишком много кода и формул. То же и с аудио, как минимум на него нужно потратить 3 строчки:
siteman.audio = new Audio(data:wav/base64);
siteman.audio.loop=true;
siteman.audio.play();

но не факт что они вот так вот сразу заработают в большинстве случаев.

И что дальше?


А ничего. Была поставлена цель сделать игру на 30 строк, на js, с помощью canvas — она выполнена. Код получился не то что бы УГ, но и не самый лучший. Зато я получил много экспы, и таки сделал какую-никакую игру.

Правда, я всё же продолжил бы делать лучше, особенно хотелось бы добавить простой ИИ, который будет отскакивать от стенок и двигаться рандомно, но для этого нужно было бы серьёзно переписать код, да и не факт что влез бы в 30 строк более-менее читаемого кода. Ну или всё же закончил бы идею с пожирателем сайта. Но жалко времени, всего на этот проект было потрачено 5 часов и не хотелось потратить на него ещё пару вечеров.

P.S. Читал саркастическую статью «js-код всего в одну строку», и имхо — смысл не в том что бы делать ровно N строк (большинство языков позволяет себя хоть в 1 строку сжать), а смысл в том, что бы написать что-то забавное и маленькое.
Tags:
Hubs:
-17
Comments 16
Comments Comments 16

Articles