05 июля 2010

Ведение лога в играх.


Всем привет. Решил я немного наполнить журнал всякой писаниной по поводу программирования и всего такого. Но здесь меня поджидала сложность: с одной стороны текст хочется сделать интересным, а код прозрачным; с другой - у меня уже довольно много самописных модулей, от которых не хочется отказываться при написании демок или при разъяснении работы тех или иных хитростей программирования. Потому, чтобы всем было просто и уютно, я решил сначала начать с маленьких модулей, расписать их назначение и работу, а затем уже с чистой совестью подключать при необходимости в демонстрационных программах не боясь вопросов "а что это за модуль ты подключил?" Итак, начну с самого простого - ведения лога в играх. Что это?


По-простому: файл в который программа записывает информацию о своей работе. Зачем это нужно? Не знаю, бывал ли у вас случай (если нет - вам в какой-то степени повезло) слышать фразу: "твоя программа у меня не запустилась, вывелось сообщение что-то вроде Access violation" - очень и очень неприятно. В такие моменты чувствуешь свое бессилие - ведь по такой информации определить почему программа "упала" практически невозможно. Начинаешь задавать вопросы что-то вроде "а в какой момент? что при этом нажимал?" и т.д. Как вы все понимаете - юзер меньше всего знает, почему программа не работает и что вообще она делала в этот момент. Грамотным в такой момент будет ответ вроде: "пришли мне лог-файл, лежащий в папке трам-пам-пам".
Когда-то давно и я сделал для себя модуль, умеющий сохранять всякую информацию на диск.
Его можно найти по ссылке.
В этом  модуле вы найдете один-единственный класс TLogManager:

TLogManager = class
  protected
    fFileName: String;
    fLogStrings: TStringList;
    fLastLogString: Integer;

    fLastSaveTime: Single;
    fReSaveTime: Single;
    Procedure SaveLogToStream(aFileStream: TStream);
    Procedure ReCreateFile;
  public
    Procedure SaveLogToFile;
    Procedure AddStringToLog(aString: String);
    Procedure DropStringToLog(aString: String);
    Procedure AddTimeToLog;
    Procedure DoProgress(const DeltaTime: Single);
    Constructor Create(const aFileName: String; const aReCreateFile: Boolean);
    Destructor Destroy; override;
  end; 
 
и три функции:
 
  Procedure Drop_StringToLog(aString: String);
  Procedure Add_StringToLog(aString: String);
  Procedure Set_LogManager(aLogManager: TLogManager);

Как это все работает? Очень просто!
Первым дело необходимо создать основной объект, который будет заниматься логом:

fLog := TLogManager.Create('log.txt', True); 
 
Думаю, здесь все ясно. Мы создаем объект класса TLogManager, указываем файл log.txt в качестве файла для записи и выставляем флаг пересоздания файла (а не дописывания в конец) в значение "правда".
Теперь осталось установить этот объект в качестве основного объекта логов:

Set_LogManager(fLog)

Все! Настройка закончена. 
Также важно позаботиться о регулярном вызове метода fLog.DoProgress(DeltaTime). Для чего? Штука в том, что доступ к файлам на жестком диске не всегда быстрое дело. Поэтому в данном случае используется следующая схема: сначала весь текст лога хранится в оперативной памяти, и только изредка (сейчас в коде жестко прописаны 30 секунд) вся накопившаяся информация записывается в файл лога. 

Теперь из любого места программы можно вызвать:
  • Drop_StringToLog(), если мы хотим тут же записать данные в лог-файл (выполняется перед критическими секциями, в которых возможен "вылет программы")
  • Add_StringToLog(), если мы хотим отложить запись данных в лог.
Вот и всё. Просто, быстро и удобно. Не забудьте также уничтожить объект перед закрытием программы. Этот модуль я писал довольно давно и до сих пор им пользуюсь. Он более-менее безопасный: при отсутствие объекта для логирования информации, функции по добавлению информации просто не будут ничего выполнять.
Что же можно записывать в лог-файл? Да что угодно! ФПС вашей игры; уровни, в которые играл юзер в эту сессию; время прохождения каждого уровня; конфигурацию компьютера; отсутствие текстур/звуков/моделей игры; лицензионный ключ, используемый для игры и т.д!
Были у меня идеи и по созданию объекта в инициализационной секции, также витала идея насчет обработки сохранения информации по тикам внутреннего таймера - но все это по тем или иным причинам было откинуто. Возможно, когда-то наступит необходимость усовершенствования этого класса, но пока, вроде бы, все уютно. А вам как кажется?

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

Рад любым идеям, предложениям и пожеланиям.

    Сообщения, схожие по тематике:

    6 коммент.:

    1. А ведь в GLScene есть свой компонент-логер ;)

      ОтветитьУдалить
    2. Yarov, угу, но он вроде новый совсем - я еще не пробовал... мой вариант наипростейшего лога был написан очень-преочень давно.
      а вообще - спасибо за наводку, гляну как будет время!

      ОтветитьУдалить
    3. В чем причина отказа от секций initialization/finalization?

      Умеет ли логгер писать критикал ерроры, сбрасывает ли он отчет принудительно при таких ошибках и/или аварийных завершениях программы?

      Это риторические вопросы, сам пытаюсь выкроить время, что бы с этими проблемами разобраться. Вкупе с многофункциональной ните-независимой консолью для дебага, чтобы не терять контроль над приложением, даже если оно зависло :)

      ОтветитьУдалить
    4. perfect daemon, лично мне очень не нравится скрывать создание/убийство объектов в initialization/finalization. я просто не люблю с ними работать, оттого бывает вводят в ступор какие-то баги при закрытии программы, связанные с тем, что я торможу, и не проверяю finalization-часть. наверно, такую мою нелюбовь можно сравнить с детской травмой))... вроде ничего серьезного, а вроде и не использую никогда.

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

      насчет консоли в отдельной нити - это круто, я пока в этом не силен, да и задачи у меня скромнее: безумные краши с неясными причинами не случались уже давно.

      p.s. хотя есть один баг в виде краша - никак отловить не могу, так как проявляется на чужом компе и только при одном извращенном условии, которое я в программе отрубил в итоге. из-за этого на всех остальных компах утекает где-то 1kb памяти за всю игру, зато полностью избавился от краша на "избранном" компе))..

      ОтветитьУдалить
    5. Я начал думать об консоли в отдельной нити, а также о навороченном логгере, когда начал писать движок, архитектура которого предполагает передачу классов, записей и указателей из dll-ки рендера через dll-ку ядра в dll-ку, в которой пишется основной код игры. Передача классов без COM и интерфейсов, все на абстрактных классах. Именно там, в условиях трех адресных пространств (не считая запустившего все это ехе) я столкнулся с тем, что отлаживать неуловимые баги трудно, в особенности баги с неверной передачей ссылок на поля типа объект при приведении класса к абстрактному. (Одно радует, для конечного пользователя нет нужды знать это)

      Говоря проще, когда архитектура, такая хорошая, простая и гибкая снаружи, стала приносить кучу проблем внутри при реализации я понял, что ни черта не знаю о дебаге неуловимых или трудно угадываемых багов :)

      ОтветитьУдалить
    6. о-о-о... все держать в отдельных dll-ках - это круто! не всегда понимаю, зачем такое может понадобиться в играх (все-таки скрипты сейчас очень быстрые), но сам факт существования такой системы заслуживает уважения!
      мне с таким пока не доводилось встречаться, так что задачи писать такую хитрую систему дебага не возникало.
      это все, как я понимаю, используется в вашем движке? вроде анонсировано, что вы пока закрыли доступ к проекту из вне... когда планируете в массы, что называется, свое творение показывать? очень любопытно глянуть, если честно!

      ОтветитьУдалить