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

Добавляем вывод количества просмотров записи из 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 семплов.

Блог Евгения Жирнова