Список постов в категории "Программирование"

Добавляем вывод количества просмотров записи из Jetpack Stats

Всем привет! У каждой записи в моем блоге есть метаинформация, где присутствует цифра с глазиком — это общее количество просмотров записи. Получается эта информация из плагина Jetpack. Сначала использовал специальный плагин, который вытаскивает эту информацию самостоятельно, потом, подсмотрев как он работает, решил сделать это сам. Получилось не сразу. Рассказываю как всё было.

Попытка №1 — решение в лоб с кешированием

Получение количества просмотров работает чрезвычайно медленно. Например, некоторые мои страницы при работе напрямую с плагином загружались аж ПЯТЬДЕСЯТ ТРИ (!!!) секунды. Поэтому в один прекрасный день я выключил этот функционал. Код был такой (functions.php), не используйте его, он здесь только для истории:

# Используем класс WPCOM_Stats из Jetpack
use Automattic\Jetpack\Stats\WPCOM_Stats;

# Метод получения количества просмотров с кэшированием
function get_post_views($post_id) {
  # Ключ кэша "wp_cached_counter_номер_поста"
  $cache_key = 'wp_cached_counter_' . $post_id;

  # Получим значение кэша по ключу
  $counter = get_transient($cache_key);

  # Если значения нет, значит создадим
  if (false == $counter) {
    # Объект класса для работы со статистикой Jetpack
    $wp_stats = new WPCOM_Stats();

    # Получим статистику просмотров для $post_id и преобразуем её в объект (тут может пройти до ДЕСЯТИ секунд)
    $stats = $wp_stats->convert_stats_array_to_object($wp_stats->get_post_views($post_id));

    # Количество просмотров
    $counter = intval($stats->views);

    # Сохраним в кэше на час
    set_transient($cache_key, $counter, HOUR_IN_SECONDS);
  }

  return $counter;
}

# Печатаем количество просмотров поста
function print_view_counter() {
  # Получим значение
  $counter = get_post_views(get_the_ID());

  # Нулевые счётчики не выводим
  if ($counter) {
    return;
  }

  # Если больше тысячи, выведем количество тысяч с суффиксом K. То есть вместо 6404 будет 6K
  if (intval($counter) > 1000) {
    printf('<span class="view" title="%s">%sK</span>', $counter, intdiv($counter, 1000));
  }
  else {
    printf('<span class="view">%s</span>', $counter);
  }
}

Затем вставляем в любом месте темы код:

<?php print_view_counter(); ?>

и получаем цифру просмотров.

Спустя какое-то время я увидел, что страницы грузятся безбожно долго, особенно страницы с категориями или тегами. Некоторые грузились по минуте, потому что запрос количества просмотров идёт с сайта stats.wp.com и иногда доходит до ДЕСЯТИ СЕКУНД на запрос.

Попытка №2 — использование WPCron без кеширования

В итоге я решил запускать обновление счётчиков периодически по крону, записывать/получать их значения с помощью update_post_meta и get_post_meta соответственно в поле _post_counter. В моём самописном плагине я ежедневно обновляю информацию:

# Используем Jetpack WPCOM_Stats
use Automattic\Jetpack\Stats\WPCOM_Stats;

# Метод получения счётчика без кеширования, возвращает цифру
function b2k_get_post_counter($wp_stats, $post_id) {
  $stats = $wp_stats->convert_stats_array_to_object($wp_stats->get_post_views($post_id));
  return intval($stats->views);
}

# Обновление счётчиков у всех страниц в блоге
function b2k_update_all_counters() {
  $posts = get_posts(
    array(
      'post_type' => 'any',
      'posts_per_page' => -1,
      'post_status' => 'publish'
    )
  );

  # Создадим один раз объект для запроса статистики
  $wp_stats = new WPCOM_Stats();

  # Для каждой страницы
  foreach ($posts as $post) {
    # Запрашиваем счётчик
    $counter = b2k_get_post_counter($wp_stats, $post->ID);
    # Записываем его в мета поле с именем _post_counter каждой странице
    update_post_meta($post->ID, '_post_counter', $counter);
  }
}
# Создаём событие b2k_update_counters_event
add_action('b2k_update_counters_event', 'b2k_update_all_counters');

# Во время активации плагина
function b2k_cron_activation() {
  # Убираем хук b2k_update_counters_event
  wp_clear_scheduled_hook('b2k_update_counters_event');
  # Добавляем ежедневный запуск события b2k_update_counters_event
  wp_schedule_event(time(), 'daily', 'b2k_update_counters_event');
}
register_activation_hook(__FILE__, 'b2k_cron_activation');

# Во время деактивации плагина
function b2k_cron_deactivation() {
  # Удаляем хук
  wp_clear_scheduled_hook('b2k_update_counters_event');
}
register_deactivation_hook(__FILE__, 'b2k_cron_deactivation');

В теме я просто получаю значение поля:

function print_view_counter() {
  # Получить значение мета поля _post_counter, всегда строка
  $counter = get_post_meta($post_id, "_post_counter", true);
  # Если счётчика нет, значит ноль
  if (!$counter) {
    $counter = 0;
  }
  else {
    # Иначе нам нужна цифра
    $counter = intval($counter);
  }

  # Если больше тысячи, выведем количество тысяч с суффиксом K. То есть вместо 6404 будет 6K
  if ($counter > 1000) {
    printf('<span class="view" title="%s">%sK</span>', $counter, intdiv($counter, 1000));
  }
  else {
    printf('<span class="view">%s</span>', $counter);
  }
}

В вычислении скорости загрузи меня выручил плагин Query Monitor. Для работы кода обязателен плагин Jetpack с включенным модулем Stats. Для первоначальной установки Cron события необходимо выключить и включить плагин.

Устройство движка для квестов Trickster Games

Всем привет! В результате недавних трудов у меня появились исходники движка для квестов компании Trickster Games. Выкладывать я их не буду, потому что не знаю насколько он лицензионно чистый. Я не юрист, в таких вопросах не разбираюсь. Поэтому расскажу своими словами — как устроен движок для квестов Trickster Games.

Поколений движков было два — первый использовался для игр Танита. Морское Приключение, Приключения Барона Мюнхгаузена на Луне и использовал lua в качестве скриптового языка.

Танита. Морское приключение и Приключения Барона Мюнхгаузена на Луне

Начиная с игры «Петрович и все, все, все» движок переходит на использование Python 2.xx вместо lua. Я пришёл в Trickster Games, когда начали делать Петрович, так что про предыдущие движки и игры практически ничего не знаю.

Итак, приступим!

Описание редактора игрового движка Trickster Games

Небольшое отступление: движок казался гораздо проще. Однако, он содержит в себе множество разных python классов, буду заново разбираться по ходу написания статьи.

Итак, у нас есть редактор (это первый уровень игры «Петрович и все все все»):

Редактор Trickster Games

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

Почти все объекты наследуются от ItemBase и рисуются относительно координат родителя. При перемещении родительского элемента на сцене, его дети перемещаются вместе с ним. В дереве объектов элементы можно перетаскивать с помощью drag-n-drop или добавлять новые правой кнопкой мыши. У каждого элемента есть замочек и глазик — блокировать перемещение/выделение при клике на сцену и показать/спрятать элемент при отрисовке сцены. Отрисовка элементов идёт по дереву от корневого узла вниз.

Уровень (Location)

Собственно уровень игры, рутовый элемент нашей сцены, содержит параметры width, height — ширина локации, высота локации. Вот её панель свойств:

Свойства локации в игровом редакторе Trickster Games

Может содержать в качестве детей только слои.

Слой (Layer)

Слой локации, имеет параметр parallax — это отставание при прокрутке. С помощью него создаётся эффект глубины при скроллинге локации по горизонтали или вертикали. Вот её панель свойств:
Свойства слоя в игровом редакторе Trickster Games Может содержать в качестве детей статические изображения, регионы, пути, анимированные объекты и точки.

Статическое изображение (LayerImage)

Фоновое изображение, ничего не делает, просто рисуется с соответствующим свойствами (параллакс) слоя. Имеет довольно скудный набор свойств:

Свойства статического изображения в игровом редакторе Trickster Games

Анимированный объект (AnimatedObject)

Можно сказать, что это наш главный элемент, базовый кирпичик нашего движка. У него есть позиция, набор состояний, к нему привязываются звуки, регионы, а также скрипт.

Свойства игрового объекта в игровом редакторе Trickster Games

Он может наследоваться от особенных классов, например, Playable Character — это наш главный герой, который умеет ходить по локации, или Takeable Item — это вещь, которую можно забрать себе в инвентарь. Он может загружаться согласно определённым условиям из скрипта, если поставить флажок «Conditional object loading». Пример условной загрузки элемента «Item_parfume» (условия описываются в родительском элементе):

class FloorLayer:
    def should_load_Item_parfume(self):
        return not world.parfume_taken

Также у него есть pivot — начало его координат и позиция на локации абсолютная и относительная. К нему могут привязываться дочерние AnimatedObject, Region и StateSequence. Содержит список состояний. Представляет собой конечный автомат, который меняет своё состояние в зависимости от внешних условий. Может загружать дочерние элементы типа AnimatedObject по некоторым условиям.

Состояние объекта (AnimatedSequence)

Анимация и состояние AnimatedObject. Анимация неотделима от состояния объекта. Пока AnimatedObject находится в определённом состоянии, проигрывается анимация этого состояния из одного или нескольких кадров. К любому кадру можно привязать автоматическое проигрывание звука.
Свойства состояния объекта в игровом редакторе Trickster Games

У состояния есть свойства on_enter, on_exit, link, on_update — для привязки пользовательских функций. on_enter выполнятся при переходе в это состояние, on_exit — при выходе из него, link — выполняется каждый кадр, и если возвращает строку, то состояние меняется на указанное, on_update вызывается каждым кадр с неким dt (количество миллисекунд между кадрами).

Регион (Region)

Некий замкнутый набор точек. Определяет область для хождения главного героя на сцене (на скриншоте редактора это R_Floor) и области для клика мышкой. Имеет свойство
«.click» или «.click_with_» для проверки клика на этот регион, второе свойство динамическое и проверяется клик с предметом из трея. Предоставляет метод для поиска кратчайшего пути между точками для хождения главного героя. Может иметь несколько типов (не помню, кто за что отвечает) и менять курсор мыши при наведении.
Свойства региона в игровом редакторе Trickster Games

Путь (Path)

Путь состоящий из нескольких точек. Можно привязать элемент к этому пути, и объект будет плавно по нему перемещаться.
Свойства пути в игровом редакторе Trickster Games

Точка (Point)

Точка на уровне. В основном используется, чтобы указывать, куда подходит главный герой для выполнения действий. Настраиваемых свойств в редакторе не имеет, только позицию.

Описание игрового движка Trickster Games

Движок написан на C++, для рендера используется DirectX 9.0, для связки Python 2.xx и C++ используется Boost.Python. Для сборки и компиляции исходников движка применяется (БУДЬТЕ ПРОКЛЯТЫ ЕГО СОЗДАТЕЛИ!) Boost Jam и Visual Studio 2007.

Базовый элемент в игровом движке — это GameObject, который имеет матрицу трансформации себя относительно родителя, список детей типа GameObject, и используется для создания дерева сцены. Простыми словами, для отрисовки дерева сцены вызывается GameObject::update(dt), который последовательно вызывает обновления всех детей, начиная от корня дерева вниз по иерархии. От него торчат уши в Python. Python-часть движка инициализируется следующим кодом:

std::string setup(
"sys.path = [r'Data\Scripts1.pak', r'Data\Scripts2.pak', "
                    "r'Data\Scripts3.pak', r'Data\Scripts3.pak\CommonClasses', "
                    "r'Data\Scripts4.pak', r'Data\Scripts4.pak\World', '.', 'Lib/std']n");
run_string(setup);

Затем вызываем метод on_init из файла Lib/engine.py:

boost::python::call<void>(bp::object(py["Lib"].attr("engine").attr("on_init")).ptr(), long(application::Application::window()));
py_on_frame = py["Lib"].attr("engine").attr("on_frame");

И после делаем on_frame из того же файла каждый кадр:

boost::python::call<void>(py_on_frame.ptr(), dt / 1000.0f, just_redraw, cursor_position, mouse_button_state);

Всё, никакой магии.

Форматы файлов

При подготовке релиза происходит преобразование .py файлов в .pyc, PNG в DDS, и всё это запаковывается в .zip файл.

Анимация

Анимация делалась в Adobe Flash и экспортировалась в набор PNG файлов, которые при запаковке релиза превращаются в DDS — это родной формат для DirectX SDK.

Звуки

Звуки — это .wav файлы

Видео

Для видео использовался формат OGV (Theora Vorbis).

Вроде всё, что мог, описал. Спрашивайте, если что-то осталось непонятным.

Рекурсивное блокирование мьютекса, знакомимся с std::recursive_mutex.

Всем привет! Случайно вспомнил, что мой блог про программирование, математику ващет и в целом создан для просвещения мешков с костями и мясом.

Поэтому хочу рассказать про <a href="https://en.cppreference.com/w/cpp/thread/recursive_mutex">std::recursive_mutex</a>. Он такой же, как и std::mutex, но запоминает номер потока, из которого был заблокирован и вторую блокировку в том же потоке игнорирует. В моём случае был приблизительно такой код:

struct Item {
    ~Item() {
        getStorage().RemoveItem(otherItemId);
        getStorage().AddItem(newItem);
    }
}
using ItemPtr = std::shared_ptr<Item>;
using ItemRef = const ItemPtr&;
using ItemId = std::size_t;

class Storage {
public:
    ItemId AddItem(ItemRef item)
    {
        std::scoped_lock lock(m_itemsGuard);
        m_items.push_back(item);
        return m_items.size() - 1;
    }

    bool HasItem(ItemId id) {
        return m_items.size() < id;
    }

    void RemoveItem(ItemId id)
    {
        std::scoped_lock lock(m_itemsGuard);
        auto item = m_items.at(id);
        m_items.erase(m_items.begin() + id);
        item.reset(); // Тут повторно заходим в RemoveItem
    }

private:
    std::vector<ItemPtr> m_items;
    std::recursive_mutex m_itemsGuard;
};


void test() {
    Storage storage;
    auto itemId = storage.AddItem(std::make_shared<Item>());
    storage.RemoveItem(itemId); // здесь проблема
}

Обратите внимание, что в деструкторе Item есть обращение к методам Storage, а там на каждое изменение массива стоит блокировка мьютекса. И оно попадет в вечный lock с std::mutex. А на этом у меня всё. Enjoy!

Знакомимся с полезными инструментами STL — std::remove() и std::remove_if()

Добрый день, всем привет!

Снова в эфире рубрика «Вы не спрашивали, но мы отвечаем», раздел «Программирование», глава «Что нового я узнал в сейчас лет».

Например, не так давно я узнал, что <a href="https://en.cppreference.com/w/cpp/container/unordered_map">std::unordered_map</a> не сортирует данные в отличие от <a href="https://en.cppreference.com/w/cpp/container/map">std::map</a>. А много-много лет назад (в 2007-м году) нам пришлось реализовывать бинарный поиск, чтобы ускорить вставку элементов в std::map (там жил кэш текстур) вместо того, чтобы взять std::unordered_map. Наш просчёт немного оправдывает то, что мы были молоды, неопытны и std::unordered_map появился спустя пять с лишним лет после описываемых событий.

Вообще, я не отношусь к тем людям, которые ежедневно читают свежайший стандарт C++, подчеркивая карандашиком важные места. Скорее, я начинаю читать стандарт, когда нужно найти решение текущей задачи. Так, например, я совершенно случайно наткнулся на <a href="https://en.cppreference.com/w/cpp/memory/enable_shared_from_this">std::enable_shared_from_this</a>, когда просматривал код нашего проекта, поминая тимлида нехорошими словами . Там был метод типа make() у класса, который должен вернуть <a href="https://en.cppreference.com/w/cpp/memory/shared_ptr">std::shared_ptr</a> от экземпляра этого класса. Если вы наследуете класс от std::enable_shared_from_this, то у вас появляется метод <a href="https://en.cppreference.com/w/cpp/memory/enable_shared_from_this/shared_from_this">shared_from_this()</a>, и дело в шляпе. Поскольку это было пять лет назад, уже не помню всех подробностей, но сначала было не очень, а потом резко стало хорошо! )

Каких-то одиннадцать лет назад я написал статью про эффективное удаление элементов массива. Тогда единственный способ, который я знал, выглядел следующим образом:

auto iter=data.begin()
while(iter != data.end()) {
    if (condition(*iter)) {
        iter = puf(data, iter);
    else {
        ++iter;
    }
}

Вся упомянутая в статье магия с перемещением в конец массива и удалением происходит в puf(). ЗдОрово, правда?

Оказывается, есть метод лучше, и имя ему — <a href="https://en.cppreference.com/w/cpp/algorithm/remove">std::remove_if()</a>, а самый интересный момент, что этот метод не удаляет элементы, а перемещает их в конец массива, возвращая итератор на начало этого кладбища погибших элементов. Между прочим, эксперимент показал, что после итератора лежит мусор.

С новыми знаниями, удаление элементов из контейнера будет выглядеть так:

auto iter = std::remove_if(data.begin(), data.end(), condition);
data.erase(iter, iter.end());

Что является более продвинутым вариантом и пишется короче, с чем я вас и поздравляю!

Фунция std::remove_if() удаляет элементы, для которых условие истинно, а std::remove() удаляет элементы с конкретным значением.

Пару слов про компьютерный звук

Компьютерный звук в компьютере представлен в виде (сюрприз-сюрприз!) массива чисел. Минимальная единица звука в компьютере — это семпл — одно число. Частота дискретизации — это количество семплов в одной секунде. Всем известная цифра 44100 — это всего лишь сорок четыре тысячи сто семплов в секунду. Семпл может быть в разных форматах и их достаточно много, чтобы описывать в этой статье. Лично я встречался с форматом float32 и с одним байтом (std::uint8_t) на семпл.

Расскажу в общих словах алгоритм проигрывание звука на компьютере. Я так делал, друзья делали всем понравилось и, в целом, везде одинаковый принцип. Алгоритм такой:

  1. Создать два буфера (A и B) в нужном формате
  2. Заполнить буфер A семплами
  3. Отдать буфер A на проигрывание звуковой карте
  4. Заполнить буфер B семплами
  5. Отдать буфер B на проигрывание звуковой карте
  6. Перейти к шагу 2.

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

Буферов должно быть не меньше двух, но может быть и гораздо больше. Как правило буфер для проигрывания — это указатель на кусок памяти и его длина в семплах или байтах. Про размер буфера ничего не скажу — я встречал размер буфер как 512 байт, так и миллион (один мегабайт) и все работали вроде бы одинаково. Возможно, кто-нибудь знает тру размер буфера и будет так любезен, чтобы написать о нём в комментариях.

«А как же второй канал, стерео и звук вокруг?», — спросит внимательный читатель. Лично я сталкивался только со стерео звуком, поэтому про пяти и более канальный звук не расскажу. Семплы для стерео звука располагаются в буфере interleaved, по очереди: LRLRLRLR. А значит для стерео звука количество семплов на секунду в буфере удваивается. И на одну секунду для частоты 44100 надо уже 88200 семплов.

Личный блог Евгения Жирнова