Pixi.js: игра «жизнь»

Продолжаю на досуге изучать 2D движек для работы с графикой на javascript. Решил написать игру «жизнь» со следующими правилами:

У каждого жителя есть возраст,пол и профессия.

  • Жители двигаются по всему полю хаотично
  • При достижении возраста 120 лет житель умирает
  • Если два жителя сталкиваются между собой и они разного пола от 18 до 50 лет, тогда они рожают ребенка

Сначала определим объекты карты:

// Люди
class People {
    /**
     * Конструктор
     * @param {type} gender - пол
     * @param {type} age - возраст
     * @param {type} health - здоровье
     * @param {type} profession - профессия из map professions
     * @returns {People}
     */
  constructor(gender,age,health,profession) {
      this.gender=gender;           // пол 0/1 м/ж
      this.age=age;                 // возраст
      this.health=health;           // здоровье
      this.profession=profession;   // профессия      
  }  
}
class Citizen {
    constructor(name,pers,color) {
      this.name=name;
      this.pers=pers; // изначальныый процент от общего колиества
      this.color=color;
    }
    
}
// Зданиям
class Building {
    /**
     * Конструктор
     * @param {type} name   название
     * @param {type} cnt    количество зданий на 1000 жителей
     * @returns {Building}
     */
    constructor(name,color,cnt) {
      this.name=name;
      this.color=color;
      this.cnt=cnt;
    }
    /**
     * Установить начальные координаты
     * @param {type} x
     * @param {type} y
     * @param {type} height
     * @param {type} width
     * @returns {undefined}
     */
    setSpawn(x,y,height,width){
      this.x=x;  
      this.y=y;
      this.height=height;
      this.width=width;        
    }
}

Зададим переменные для игры:


/**
 * Профессии, они же модели поведения
 * @type Map
 */
professions=new Map(
        [
            // индекс, название,  % от всего населения
            [1,new Citizen("Детсадовец",10,0xFFFF0B)],    // ночует дома, днём ходит в детский сад
            [2,new Citizen("Школьник",10,0xAA0000)],      // ночует дома, днем до 13:00 в школе, потом идёт домой через магазин
            [3,new Citizen("Студент",10,0xFFFFFF)],       // ночует дома, днем до 17:00 в институте, потом идёт домой через магазин
            [4,new Citizen("Продавец",20,0xFF0000)],      // ночует дома, днём в магазине, потом идёт домой через магазин
            [5,new Citizen("Офисный клерк",20,0xFF3300)], // ночует дома, днём в офисе, потом идёт домой через магазин
            [6,new Citizen("Рабочий",20,0x0000FF)],       // ночует дома, днём на заводе, потом идёт домой через магазин
            [7,new Citizen("Пенсионер",10,0xDE3249)]      // ночует дома, днём шляется по магазинам или по улицам
        ]
);
buldings_types=new Map(
        [   
            [1,new Building("Жилище",0xDE3249,10)], // индекс, название, количество на 1000 жителей
            [2,new Building("Школа",0xAA0000,1)],
            [3,new Building("Университет",0xFFFFFF,1)],
            [4,new Building("Магазин",0xFF0000,2)],
            [5,new Building("Офис",0xFF3300,3)],
            [6,new Building("Завод",0x0000FF,1)],                
            [7,new Building("Детсад",0xFFFF0B,1)]                
        ]
);


// ну пусть размеры пока в экран, потом можно поиграться
var scrollWidth=GetScrollWidth();
var screen_width = window.innerWidth-4; //получаем ширину экрана
var screen_height = window.innerHeight-4; // получаем высоту экрана

residents_count=1000;   // начальное количество жителей
residents=[];           // массив жителей
buldings=[];            // массив зданий
cur_seconds=0;          // текущая секунда
cur_minuts=0;           // текущая минута
cur_hour=0;             // текущая час
cur_day=0;              // текущая день
cur_month=0;            // текущая месяц
cur_year=0;             // текущая год

max_FPS=60;

Напишем процедуры генерации зданий и жителей:

function GenerateNewCitizen(){
   x=randomIntFromInterval(1,screen_width-width);
   y=randomIntFromInterval(1,screen_height-height);      
   resident=new People(randomIntFromInterval(0,1),1,randomIntFromInterval(1,100),professions.get(1));
   
   graphics = new PIXI.Graphics();           
   graphics.beginFill(professions.get(1).color);
   graphics.lineStyle(2, professions.get(1).color, 1);
   graphics.beginFill(professions.get(1).color, 1);           
   graphics.drawCircle(0,0, 1);  
   graphics.position.set(x, y);         
   graphics.direction=randomIntFromInterval(0,360);
   graphics.endFill();        
   graphics.resident=resident;
   residents.push(graphics);

   app.stage.addChild(residents[residents.length-1]); 
    
}

/**
 * Начальная генерация зданий
 * @returns {undefined}
 */
function InitGenerateBuilding(){
  for (let i=1;i<= buldings_types.size;i++){
      cnt=buldings_types.get(i).cnt/1000*residents_count;
      yet_cnt=1;
      while (yet_cnt<=cnt){
        // случайным образом генерируем координаты и размеры здания
        width=randomIntFromInterval(20,60);
        height=randomIntFromInterval(20,60);
        x=randomIntFromInterval(1,screen_width-width);
        y=randomIntFromInterval(1,screen_height-height);      
        // если наложений нет, то добавляю здание в массив
        if (CheckingOverlaysRect(x,y,width,height,buldings)==false){
                console.log("Добавляю "+buldings_types.get(i).name+" №"+yet_cnt);
                graphics = new PIXI.Graphics();  
                graphics.lineStyle(2, buldings_types.get(i).color, 1);
                graphics.drawRect(x, y, width, height);
                buldings.push(graphics); 
            app.stage.addChild(buldings[buldings.length-1]);            
                FPSText = new PIXI.Text(buldings_types.get(i).name,new PIXI.TextStyle({fontFamily: 'Arial',fontSize: 11,}));
                FPSText.x = x;
                FPSText.y = y;            
            app.stage.addChild(FPSText);         
            yet_cnt++;
        };
      }
  };  
}

/**
 * Проверка наложения квадратов друг на друга
 * @param {type} x
 * @param {type} y
 * @param {type} width
 * @param {type} height
 * @param {type} rects
 * @returns {res|Boolean}
 */
function CheckingOverlaysRect(x,y,width,height,rects){
  res=false;
    for (let bi=1;bi<= rects.length;bi++){
         rect_height=rects[bi-1].getBounds().height;
         rect_width=rects[bi-1].getBounds().width;
         rect_x=rects[bi-1].getBounds().x;
         rect_y=rects[bi-1].getBounds().y;
         //console.log(rect_height,rect_width,rect_x,rect_y);
         if ((x>=rect_x)&(x<=rect_x+rect_width)){res=true;};
         if ((y>=rect_y)&(y<=rect_y+rect_height)){res=true;};
         if ((x+width>=rect_x)&(x+width<=rect_x+rect_width)){res=true;};
         if ((y+height>=rect_y)&(y+height<=rect_y+rect_height)){res=true;};         
    };  
  return res;
}

/**
 * Начальная генерация жителей
 * @returns {undefined}
 */
function InitGeneratePeoples(){
        
    for (let i=1;i<= professions.size;i++){ //перебираем все профессии
        console.log("-генерирую "+professions.get(i).name);
        for (let cnt=1;cnt<= professions.get(i).pers*residents_count/100;cnt++){
          x=randomIntFromInterval(1,screen_width-width);
          y=randomIntFromInterval(1,screen_height-height);      
          resident=new People(randomIntFromInterval(0,1),randomIntFromInterval(1,100),randomIntFromInterval(1,100),professions.get(i));


           graphics = new PIXI.Graphics();           
           graphics.beginFill(professions.get(i).color);
           graphics.lineStyle(2, professions.get(i).color, 1);
           graphics.beginFill(professions.get(i).color, 1);           
           graphics.drawCircle(0,0, 1);  
           graphics.position.set(x, y);         
           graphics.direction=randomIntFromInterval(0,360);
           graphics.endFill();        
           graphics.resident=resident;
           residents.push(graphics);

        app.stage.addChild(residents[residents.length-1]); 
          
          
          
        }
    }    
}

Создадим сцену и запустим время:

const app = new PIXI.Application({ 
    width:screen_width,
    height:screen_height,
    background: '#1099bb' ,
    antialias: true
});
document.body.appendChild(app.view);

app.stage.hitArea = app.screen;

InitGenerateBuilding(); // генерируем здания
InitGeneratePeoples();  // генерируем жителей

let ticker = PIXI.Ticker.shared;   
ticker.autoStart = false;
ticker.maxFPS=max_FPS;
ticker.start();

const style = new PIXI.TextStyle({
    fontSize: 20 
});
FPSText = new PIXI.Text('',style);
FPSText.x = 10;
FPSText.y = 10;   
app.stage.addChild(FPSText);         
StatText = new PIXI.Text(residents.length,style);
StatText.x = 10;
StatText.y = 30;   
app.stage.addChild(StatText);         


// пусть жители оживут!
ticker.add(function (time) {     
    cur_seconds++;
    if (cur_seconds==61){
        cur_seconds=0;cur_minuts++;
        // прибавляем всем жителям по году жизни..
        for (let i = 0; i < residents.length; i++) {         
            residents[i].resident.age++;
            if (residents[i].resident.age==120){
                residents.pop(residents[i]); // в 120 лет жизненный путь завершается..
            };
            
        };
    };
    if (cur_minuts==61){cur_minuts=0;cur_hour++;};
    if (cur_hour==25){cur_hour=0;cur_day++;};    
    if (cur_day==366){cur_day=0;cur_year++;};       
    FPSText.text="С начала эпохи прошел: "+cur_minuts+" год"; // выводим FPS
    StatText.text="Жителей:"+residents.length;
    // перебираю каждого человека и двигаем его
    for (let i = 0; i < residents.length; i++) {         
        step=false;
        while (step==false){
            pre_y=residents[i].position.y+Math.sin(residents[i].direction);
            pre_x=residents[i].position.x+Math.cos(residents[i].direction);
            if ((pre_x>=0)&& (pre_x<=screen_width) && (pre_y>=0) && (pre_y<=screen_height)) {
                    step=true;
            } else {
               residents[i].direction=randomIntFromInterval(0,360); 
            };
            
        };
       residents[i].position.y=residents[i].position.y+Math.sin(residents[i].direction);
       residents[i].position.x=residents[i].position.x+Math.cos(residents[i].direction);
       // проверим, есть ли совпадение точек?
       // если возраст от 18..50
       // если полы противоположные
       // то считаем что это "лябовь" и размножаемся
        for (let j = 0; j < residents.length; j++) {   
            if (Math.round(residents[i].position.y)==Math.round(residents[j].position.y) && 
                    Math.round(residents[i].position.x)==Math.round(residents[j].position.x) && i!=j){
                      if (residents[i].resident.age>=18 && residents[i].resident.age<=50){
                        if (residents[j].resident.age>=18 && residents[j].resident.age<=50){
                            if (residents[j].resident.gender==0 && residents[i].resident.gender==1){
                                console.log("-это лябофь!");  
                                GenerateNewCitizen();
                            }
                        }                          
                      }               
            };
        };
    }
});

Pixi.js : первые шаги

Pixi.js — это движёк 2D графики на JavaScript. Позволяет использовать WebGL или Canvas если первый не доступен.

Ну что, попробуем его в действии. Нарисуем 2000 кружков и попробуем их подвигать.

Подключим движёк через CDN:

<script src="https://cdn.jsdelivr.net/npm/pixi.js@7.x/dist/pixi.min.js"></script>

Обьявим массив кружков, определим размер экрана:

    grand_mamas=[];
    count_grand_mamas=2000;
    
    var width = window.innerWidth; //получаем ширину экрана
    var height = window.innerHeight; // получаем высоту экрана

Инициируем экран приложения:

    const app = new PIXI.Application({ 
        width:width,
        height:height,
        background: '#1099bb' ,
        antialias: true
    });
    document.body.appendChild(app.view);

Нарисуем 2000 кружков и поместим эти обьекты в массив, чтобы дальше с ними играться:

    for (let i = 0; i < count_grand_mamas; i++) { 
        graphics = new PIXI.Graphics();           
        graphics.beginFill(0xDE3249);
        graphics.lineStyle(2, 0xFEEB77, 1);
        graphics.beginFill(0x650A5A, 1);
        graphics.drawCircle(0,0, randomIntFromInterval(1,10));       
        graphics.position.set(randomIntFromInterval(-width,width), randomIntFromInterval(1,height));
        graphics.endFill();        
        grand_mamas.push(graphics);
        app.stage.addChild(grand_mamas[i]);
        
    };

Далее используем класс ticker для периодических действий с кружками. По умолчанию класс эвент класса выполняется 60 раз в секунду. Что делаем в эвенте? Пробегаемся по массиву кружков и двигаем их. Если кружок выходит за пределы экрана, то передвигаем его случайным образом вверх экрана. Бонусом выведем FPS:

  FPSText = new PIXI.Text('FPS:0');
  FPSText.x = 10;
  FPSText.y = 10;   
  app.stage.addChild(FPSText);
   
  let ticker = PIXI.Ticker.shared;   
  ticker.autoStart = false;
  ticker.start();
  
   step=0.01;
   ticker.add(function (time) {
        FPSText.text='FPS:'+ticker.FPS;
        for (let i = 0; i < count_grand_mamas; i++) {                 
            count=count+step;
            if (count>10){step=-step;};        
            if (count<0){step=-step;};        
            grand_mamas[i].position.x=grand_mamas[i].position.x+count;
            grand_mamas[i].position.y=grand_mamas[i].position.y+count;
            //console.log(grand_mamas[i].getBounds());
            if ((grand_mamas[i].position.x>width)||(grand_mamas[i].position.y>height)){
                grand_mamas[i].position.x=randomIntFromInterval(-width,width);
                grand_mamas[i].position.y=randomIntFromInterval(1,1);
            };
        }  
   });

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

PostgreeSQL: формат даты

Для форматирования даты в стоку нужного формата в PGSQL предусмотрена функция to_char(дата,формат). Использовать в запросе можно примерно так:

select 
comments.comment,
to_char(comments.dt, 'DD.MM.YYYY HH24:MI:SS'),
ls.ls,
users.login
from comments
inner join ls on ls.id=comments.lsid
inner join users on users.id=comments.userid
where ls.ls='1234' order by dt desc limit 10

При этом поддерживаются следующие паттерны форматирования:

ПаттернОписание
HHчас дня (01-12)
HH12час дня (01-12)
HH24час дня (00-23)
MIминуты (00–59)
SSсекунды (00–59)
MSмилисекунды (000–999)
USмикросекунды (000000–999999)
FF1десятые доли секунды (0-9)
FF2сотая доля секунды (00-99)
FF3милисекунлы (000–999)
FF4десятая часть миллисекунды (0000-9999)
FF5сотая доля миллисекунды (00000-99999)
FF6микросекунды (000000–999999)
SSSS, SSSSSсекунды после полуночи (0-86399)
AM , am , PM или pmмеридианный индикатор (без периодов)
A.M. , a.m. , P.M. или p.m.меридианный индикатор (с периодами)
Y,YYYгод (4 и более цифры)с запятой
YYYYгод (4 и более цифры)
YYYпоследние 3 цифры года
YYпоследние 2 цифры года
Yпоследняя цифра года
IYYYISO 8601 год с номером недели (4 или более цифр)
IYYпоследние 3 цифры года по стандарту ISO 8601 год с номером недели
IYпоследние 2 цифры года по ISO 8601 номер недели
Iпоследняя цифра года недели по ISO 8601
BC , bc , AD или adиндикатор эры (без периодов)
B.C. , b.c. , A.D. или a.d.индикатор эры (с периодами)
MONTHполное название месяца в верхнем регистре (с пустыми кнопками до 9 символов)
Monthполное название месяца с заглавными буквами (с пустыми кнопками до 9 символов)
monthполное название месяца в нижнем регистре (с пустыми кнопками до 9 символов)
MONсокращенное название месяца в верхнем регистре (3 символа на английском языке,локализованная длина варьируется)
Monсокращенное название месяца с заглавными буквами (3 символа на английском языке,локальная длина варьируется)
monсокращенное название месяца в нижнем регистре (3 символа на английском языке,локализованная длина варьируется)
MMномер месяца (01-12)
DAYполное название дня в верхнем регистре (с пустыми кнопками до 9 символов)
Dayполное название дня с заглавной буквы (пустое,до 9 символов)
dayполное название дня в нижнем регистре (с пустыми кнопками до 9 символов)
DYсокращенное название дня в верхнем регистре (3 символа на английском языке,локализованная длина варьируется)
Dyсокращенное название дня с заглавными буквами (3 символа на английском языке,локальная длина варьируется)
dyсокращенное название дня в нижнем регистре (3 символа на английском языке,локализованная длина варьируется)
DDDдень года (001-366)
IDDDдень года с нумерацией недель ISO 8601 (001-371;день 1 года-понедельник первой недели ISO)
DDдень месяца (01-31)
Dдень недели, с воскресенья ( 1 ) по субботу ( 7 )
IDISO 8601 день недели, с понедельника ( 1 ) по воскресенье ( 7 )
Wнеделя месяца (1-5)(первая неделя начинается в первый день месяца)
WWномер недели года (1-53)(первая неделя начинается в первый день года)
IWномер недели года с нумерацией недель ISO 8601 (01-53;первый четверг года приходится на неделю 1)
CCвек (2 цифры)(XXI век начинается с 2001-01-01 гг.)
JЮлианская дата (целое число дней с 24 ноября 4714 г. до н.э. в местную полночь; см. Раздел B.7 )
Qquarter
RMмесяц римскими цифрами в верхнем регистре (I-XII;I=январь)
rmмесяц в нижнем регистре римскими цифрами (i-xii;i=январь)
TZАббревиатура часового пояса в верхнем регистре (поддерживается только в to_char )
tzаббревиатура часового пояса в нижнем регистре (поддерживается только в to_char )
TZHсмещение зоны в часах
TZMсмещение зоны в минутах
OFсмещение часового пояса от UTC (поддерживается только в to_char )

Javascript: Открыть страницу на новой вкладке, передав на неё данные через POST

Есть несколько вариантов решения данной задачи Самым простым из них является размещение на странице скрытой формы, заполнение её данными в нужный момент, и принудительный submit. Например как-то так:

<form id="my_form" method="post" action="?view=new_page" target="Новое окно">
	<input type="hidden" id="my_var" name="my_var" value="" />
</form>

<script type="text/javascript">
	my_var="Мои данные в POST";
	window.open('', 'Новое окно');
	document.getElementById('my_form').submit();
</script>

MapYandex API: получение расстояний между двумя координатами

Задача: получить расстояние от точки до точки по автомобильным дорогам

Решение: воспользуемся вызовом route:

ymaps.route([
                [data[i]["to_latlatitude"],data[i]["to_longitude"]],
                [data[i]["from_latlatitude"],data[i]["from_longitude"]]
            ], {
                mapStateAutoApply: true
            }).then(function (route) {
                console.log("Маршрут:");
                console.log(route);
                console.log(route.getLength());

            });     
1 7 8 9 10 11 56