Obiekty Gry Rozmawiają Ze Sobą

Jaki jest dobry sposób radzenia sobie z przedmiotami i zmuszania ich do rozmawiania ze sobą?

Do tej pory wszystkie Moje hobby/student gry były małe, więc ten problem został rozwiązany w dość brzydki sposób, co prowadzi do ścisłej integracji i kolistych zależności. Co było dobre jak na wielkość projektów, które robiłem.

Jednak moje projekty stają się coraz większe i bardziej złożone, a teraz chcę zacząć ponownie używać kodu, a moja głowa staje się prostsza.

The główny problem, który mam jest ogólnie wzdłuż linii Player musi wiedzieć o Map i tak robi Enemy, to zwykle zstąpił do Ustawienia wiele wskaźników i wiele zależności, A to staje się bałagan szybko.

Myślałem o systemie stylu wiadomości. ale naprawdę nie mogę zobaczyć, jak to zmniejsza zależności, jak nadal będę wysyłać wskaźniki wszędzie.

PS: chyba już o tym dyskutowano, ale nie wiem co to się nazywa potrzeba, którą mam.

Author: cubuspl42, 2011-01-01

7 answers

EDIT: poniżej opisuję podstawowy system komunikowania zdarzeń, z którego korzystałem w kółko. I przyszło mi do głowy, że oba projekty szkolne są open source i w Internecie. Możesz znaleźć drugą wersję tego systemu wiadomości (i sporo więcej) na http://sourceforge.net/projects/bpfat /.. Zapraszamy do zapoznania się z dokładniejszym opisem systemu!

Napisałem ogólny system komunikatorów i wprowadziłem go do garstki gier, które zostały wydane na PSP, a także niektóre aplikacje na poziomie przedsiębiorstwa. Celem systemu przesyłania wiadomości jest przekazywanie tylko tych danych, które są potrzebne do przetworzenia wiadomości lub zdarzenia, w zależności od terminologii, której chcesz użyć, tak aby obiekty nie musiały się o sobie nawzajem znać.

Szybkie zestawienie listy obiektów używanych do tego celu jest czymś w stylu:

struct TEventMessage
{
    int _iMessageID;
}

class IEventMessagingSystem
{
    Post(int iMessageId);
    Post(int iMessageId, float fData);
    Post(int iMessageId, int iData);
    // ...
    Post(TMessageEvent * pMessage);
    Post(int iMessageId, void * pData);
}

typedef float(*IEventMessagingSystem::Callback)(TEventMessage * pMessage);

class CEventMessagingSystem
{
    Init       ();
    DNit       ();
    Exec       (float fElapsedTime);

    Post       (TEventMessage * oMessage);

    Register   (int iMessageId, IEventMessagingSystem* pObject, FObjectCallback* fpMethod);
    Unregister (int iMessageId, IEventMessagingSystem* pObject, FObjectCallback * fpMethod);
}

#define MSG_Startup            (1)
#define MSG_Shutdown           (2)
#define MSG_PlaySound          (3)
#define MSG_HandlePlayerInput  (4)
#define MSG_NetworkMessage     (5)
#define MSG_PlayerDied         (6)
#define MSG_BeginCombat        (7)
#define MSG_EndCombat          (8)

A teraz trochę wyjaśnienia. Pierwszy obiekt, TEventMessage, jest obiektem bazowym do reprezentowania dane przesyłane przez system wiadomości. Domyślnie zawsze będzie miał identyfikator wysyłanej wiadomości, więc jeśli chcesz się upewnić, że otrzymałeś wiadomość, której oczekiwałeś, możesz (generalnie robię to tylko w debugowaniu).

Następna jest klasa interfejsu, która daje ogólny obiekt dla systemu wiadomości do użycia do odlewania podczas wykonywania wywołań zwrotnych. Dodatkowo zapewnia to również "łatwy w użyciu" interfejs do Post () ing różnych typów danych do systemu wiadomości.

Po tym mamy nasz typedef wywołania zwrotnego, po prostu oczekuje obiektu typu klasy interfejsu i przejdzie wzdłuż wskaźnika TEventMessage... Opcjonalnie możesz zrobić parametr const, ale używałem przetwarzania trickle up wcześniej dla rzeczy takich jak debugowanie stosu i takich z systemu wiadomości.

Ostatni i w rdzeniu znajduje się obiekt CEventMessagingSystem. Obiekt ten zawiera tablicę stosów obiektów wywołania zwrotnego (lub połączonych list, kolejek lub danych, które chcesz przechowywać). Na obiekty wywołania zwrotnego, nie pokazane powyżej, muszą utrzymywać (i są jednoznacznie zdefiniowane przez) wskaźnik do obiektu, jak również metodę wywołania tego obiektu. Podczas rejestracji() dodajesz wpis na stosie obiektu w pozycji tablicy ID wiadomości. Po wyrejestrowaniu() usuwasz ten wpis.

To w zasadzie wszystko. Teraz ma to zastrzeżenie, że wszystko musi wiedzieć o IEventMessagingSystem i obiekcie TEventMessage... ale ten obiekt nie powinien zmieniać, że często i tylko przekazuje te części informacji, które są istotne dla logiki dyktowanej przez wywołane zdarzenie. W ten sposób gracz nie musi wiedzieć bezpośrednio o mapie lub przeciwniku, aby wysyłać na nią wydarzenia. Zarządzany obiekt może również wywoływać API do większego systemu, bez potrzeby wiedzy o nim.

Na przykład: kiedy wróg umiera, chcesz, aby odegrał efekt dźwiękowy. Zakładając, że masz menedżera dźwięku, który dziedziczy interfejs IEventMessagingSystem, skonfigurujesz wywołanie zwrotne dla systemu wiadomości, który zaakceptowałby TEventMessagePlaySoundEffect lub coś w tym stylu. Menedżer dźwięku rejestruje to wywołanie zwrotne, gdy efekty dźwiękowe są włączone (lub wyrejestrować wywołanie zwrotne, gdy chcesz wyciszyć wszystkie efekty dźwiękowe, aby ułatwić włączanie / wyłączanie). Następnie będziesz miał obiekt wroga również dziedziczony z systemu IEventMessagingSystem, złożony z obiektu TEventMessagePlaySoundEffect (będzie potrzebował MSG_PlaySound dla jego ID wiadomości, a następnie ID efekt dźwiękowy do odtwarzania, czy to INT ID lub nazwa efektu dźwiękowego) i po prostu wywołać Post(&oeventmessageplaysoundeffect).

Teraz jest to tylko bardzo prosty projekt bez implementacji. Jeśli masz natychmiastowe wykonanie, nie musisz buforować obiektów TEventMessage (czego używałem głównie w grach konsolowych). Jeśli znajdujesz się w środowisku wielowątkowym, jest to bardzo dobrze zdefiniowany sposób, aby obiekty i systemy działające w oddzielnych wątkach mogły ze sobą rozmawiać, ale będziesz chciał aby zachować obiekty TEventMessage tak, aby dane były dostępne podczas przetwarzania.

Kolejna zmiana dotyczy obiektów, które potrzebują tylko danych Post (), można utworzyć statyczny zestaw metod w IEventMessagingSystem, aby nie musiały dziedziczyć od nich (co jest używane dla ułatwienia dostępu i zdolności wywołania zwrotnego, nie-bezpośrednio-potrzebne do połączeń Post ()).

Dla wszystkich, którzy wspominają o MVC, jest to bardzo dobry wzór, ale można go zaimplementować na wiele różnych sposobów i na różnych poziomach. Obecny projekt pracuję na profesjonalnie jest konfiguracja MVC około 3 razy, jest globalny MVC całej aplikacji, a następnie mądry projekt każdy M V i C jest również samodzielny wzorzec MVC. Więc to, co próbowałem tutaj zrobić, to wyjaśnić, jak zrobić C, który jest na tyle ogólny, aby obsłużyć prawie każdy typ M bez potrzeby wchodzenia w Widok...

Na przykład Obiekt, gdy "umiera", może chcieć odtworzyć efekt dźwiękowy.. Zrobiłbyś struktura dla systemu dźwiękowego, taka jak TEventMessageSoundEffect, która dziedziczy po TEventMessage i dodaje identyfikator efektu dźwiękowego (czy to wstępnie załadowany Int, czy nazwa pliku sfx, jednak są one śledzone w systemie). Następnie cały obiekt musi złożyć obiekt TEventMessageSoundEffect z odpowiednim szumem śmierci i wywołać Post (&oeventmessagesoundeffect); obiekt.. Zakładając, że dźwięk nie jest wyciszony (co chciałbyś wyrejestrować menedżerów dźwięku.

EDIT: To wyjaśnij to trochę w odniesieniu do poniższego komentarza: Każdy obiekt do wysłania lub odebrania wiadomości musi tylko wiedzieć o interfejsie IEventMessagingSystem i jest to jedyny obiekt, który EventMessagingSystem musi wiedzieć o wszystkich pozostałych obiektach. To jest to, co daje wam odwiązanie. Każdy obiekt, który chce otrzymać wiadomość, po prostu zarejestruj (MSG, Object, Callback) s dla niego. Następnie, gdy obiekt wywołuje Post(MSG,Data) wysyła do EventMessagingSystem za pośrednictwem interfejsu, o którym wie, EMS powiadomi każdy zarejestrowany obiekt o zdarzeniu. Możesz zrobić Msg_playeried, który obsługuje inne systemy, lub gracz może wywołać MSG_PlaySound, MSG_Respawn, itp, aby rzeczy nasłuchujące tych wiadomości mogły na nich działać. Pomyśl o poście (MSG, Data)jako abstrakcyjnym API do różnych systemów w silniku gry.

Oh! Jeszcze jedna rzecz, która mi wskazała. System, który opisuję powyżej, pasuje do wzorca obserwatora w drugiej udzielonej odpowiedzi. Więc jeśli chcesz bardziej ogólne opis aby mój miał trochę więcej sensu, jest to krótki artykuł, który daje mu dobry opis.

Mam nadzieję, że to pomoże i cieszyć się!

 41
Author: James,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-31 15:19:56

Ogólne rozwiązania komunikacji między obiektami unikające ciasnego sprzężenia:

  1. Wzór mediatora
  2. Obserwator wzór
 15
Author: Stephane Rolland,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-01-26 12:19:35

Prawdopodobnie dotyczy to nie tylko klas gry, ale także klas w ogólnym znaczeniu. wzorzec MVC (model-view-controller) wraz z sugerowaną pompą komunikatów to wszystko, czego potrzebujesz.

"Enemy " I" Player " prawdopodobnie pasują do modelowej części MVC, nie ma to większego znaczenia, ale zasada jest taka, że wszystkie modele i widoki oddziałują za pośrednictwem kontrolera. Tak więc, powinieneś zachować referencje (lepsze niż wskaźniki) do (prawie) wszystkich innych instancji klasy z tego 'kontrolera' klaso, nazwijmy to kontrolerem. Dodaj do niego pompę komunikatów (różni się w zależności od platformy, dla której kodujesz), najpierw Utwórz instancję (przed jakąkolwiek inną klasą, A inne obiekty będą jej częścią) lub na koniec (a inne obiekty będą przechowywane jako odniesienia w ControlDispatcher).

Oczywiście Klasa ControlDispatcher prawdopodobnie będzie musiała zostać podzielona dalej na bardziej wyspecjalizowane Kontrolery tylko po to, aby utrzymać kod na pliku w okolicach 700-800 linii (jest to limit dla mnie przynajmniej) i może nawet mieć więcej wątków pompowania i przetwarzania wiadomości w zależności od potrzeb.

Cheers

 4
Author: kellogs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-01-01 12:32:27

Oto zgrabny system zdarzeń napisany dla C++11, którego możesz użyć. Używa szablonów i inteligentnych wskaźników, a także Lambda dla delegatów. Jest bardzo elastyczny. Poniżej znajduje się również przykład. Napisz do mnie na [email protected] jeśli masz pytania na ten temat.

To, co te klasy dają, to sposób wysyłania zdarzeń z dołączonymi do nich dowolnymi danymi oraz łatwy sposób bezpośredniego wiązania funkcji, które akceptują już przekonwertowane typy argumentów, które System rzuca i sprawdza pod kątem poprawności konwersja przed wywołaniem delegata.

Zasadniczo każde zdarzenie pochodzi z klasy IEventData(możesz ją nazwać IEvent, jeśli chcesz). Każda" ramka " wywołana przez ProcessEvents (), w którym to momencie System zdarzeń zapętla wszystkie delegaty i wywołuje delegaty dostarczone przez inne systemy, które subskrybowały każdy typ zdarzenia. Każdy może wybrać, które wydarzenia chcą subskrybować, ponieważ każdy typ Wydarzenia ma unikalny identyfikator. Możesz również użyć lambda, aby zapisać się do zdarzenia takie jak: AddListener (MyEvent:: ID (), [&] (shared_ptr ev){ rób swoje..

W każdym razie, oto klasa z całą implementacją:

#pragma once

#include <list>
#include <memory>
#include <map>
#include <vector>
#include <functional>

class IEventData {
public:
    typedef size_t id_t; 
    virtual id_t GetID() = 0; 
}; 

typedef std::shared_ptr<IEventData> IEventDataPtr; 
typedef std::function<void(IEventDataPtr&)> EventDelegate; 

class IEventManager {
public:
    virtual bool AddListener(IEventData::id_t id, EventDelegate proc) = 0;
    virtual bool RemoveListener(IEventData::id_t id, EventDelegate proc) = 0; 
    virtual void QueueEvent(IEventDataPtr ev) = 0; 
    virtual void ProcessEvents() = 0; 
}; 


#define DECLARE_EVENT(type) \
    static IEventData::id_t ID(){ \
        return reinterpret_cast<IEventData::id_t>(&ID); \
    } \
    IEventData::id_t GetID() override { \
        return ID(); \
    }\

class EventManager : public IEventManager {
public:
    typedef std::list<EventDelegate> EventDelegateList; 

    ~EventManager(){
    } 
    //! Adds a listener to the event. The listener should invalidate itself when it needs to be removed. 
    virtual bool AddListener(IEventData::id_t id, EventDelegate proc) override; 

    //! Removes the specified delegate from the list
    virtual bool RemoveListener(IEventData::id_t id, EventDelegate proc) override; 

    //! Queues an event to be processed during the next update
    virtual void QueueEvent(IEventDataPtr ev) override; 

    //! Processes all events
    virtual void ProcessEvents() override; 
private:
    std::list<std::shared_ptr<IEventData>> mEventQueue; 
    std::map<IEventData::id_t, EventDelegateList> mEventListeners; 

}; 

//! Helper class that automatically handles removal of individual event listeners registered using OnEvent() member function upon destruction of an object derived from this class. 
class EventListener {
public:
    //! Template function that also converts the event into the right data type before calling the event listener. 
    template<class T>
    bool OnEvent(std::function<void(std::shared_ptr<T>)> proc){
        return OnEvent(T::ID(), [&, proc](IEventDataPtr data){
            auto ev = std::dynamic_pointer_cast<T>(data); 
            if(ev) proc(ev); 
        }); 
    }
protected:
    typedef std::pair<IEventData::id_t, EventDelegate> _EvPair; 
    EventListener(std::weak_ptr<IEventManager> mgr):_els_mEventManager(mgr){

    }
    virtual ~EventListener(){
        if(_els_mEventManager.expired()) return; 
        auto em = _els_mEventManager.lock(); 
        for(auto i : _els_mLocalEvents){
            em->RemoveListener(i.first, i.second); 
        }
    }

    bool OnEvent(IEventData::id_t id, EventDelegate proc){
        if(_els_mEventManager.expired()) return false; 
        auto em = _els_mEventManager.lock(); 
        if(em->AddListener(id, proc)){
            _els_mLocalEvents.push_back(_EvPair(id, proc)); 
        }
    }
private:
    std::weak_ptr<IEventManager> _els_mEventManager; 
    std::vector<_EvPair>        _els_mLocalEvents; 
    //std::vector<_DynEvPair> mDynamicLocalEvents; 
}; 

Oraz plik Cpp:

#include "Events.hpp"

using namespace std; 

bool EventManager::AddListener(IEventData::id_t id, EventDelegate proc){
    auto i = mEventListeners.find(id); 
    if(i == mEventListeners.end()){
        mEventListeners[id] = list<EventDelegate>(); 
    }
    auto &list = mEventListeners[id]; 
    for(auto i = list.begin(); i != list.end(); i++){
        EventDelegate &func = *i; 
        if(func.target<EventDelegate>() == proc.target<EventDelegate>()) 
            return false; 
    }
    list.push_back(proc); 
}

bool EventManager::RemoveListener(IEventData::id_t id, EventDelegate proc){
    auto j = mEventListeners.find(id); 
    if(j == mEventListeners.end()) return false; 
    auto &list = j->second; 
    for(auto i = list.begin(); i != list.end(); ++i){
        EventDelegate &func = *i; 
        if(func.target<EventDelegate>() == proc.target<EventDelegate>()) {
            list.erase(i); 
            return true; 
        }
    }
    return false; 
}

void EventManager::QueueEvent(IEventDataPtr ev) {
    mEventQueue.push_back(ev); 
}

void EventManager::ProcessEvents(){
    size_t count = mEventQueue.size(); 
    for(auto it = mEventQueue.begin(); it != mEventQueue.end(); ++it){
        printf("Processing event..\n"); 
        if(!count) break; 
        auto &i = *it; 
        auto listeners = mEventListeners.find(i->GetID()); 
        if(listeners != mEventListeners.end()){
            // Call listeners
            for(auto l : listeners->second){
                l(i); 
            }
        }
        // remove event
        it = mEventQueue.erase(it); 
        count--; 
    }
}

Używam klasy EventListener dla wygody jako klasy bazowej dla każdej klasy, która chciałaby słuchać wydarzeń. Jeśli korzystasz z tej klasy słuchania i dostarczasz ją do swojego event managera, możesz skorzystać z bardzo wygodnej funkcji OnEvent(..), aby zarejestrować swoje wydarzenia. A klasa bazowa automatycznie anuluje subskrypcję klasy pochodnej ze wszystkich zdarzeń, gdy zostanie zniszczona. Jest to bardzo wygodne, ponieważ zapomnienie o usunięciu delegata z menedżera zdarzeń, gdy klasa zostanie zniszczona, prawie na pewno spowoduje awarię programu.

Zgrabny sposób na uzyskanie unikalnego identyfikatora typu dla zdarzenia poprzez zwyczajne zadeklarowanie statycznej funkcji w klasie, a następnie wrzucenie jej adresu do int. Ponieważ każda klasa będzie miała tę metodę pod różnymi adresami, może być służy do unikalnej identyfikacji zdarzeń klasowych. Możesz również dodać typename () do int, aby uzyskać unikalne id, jeśli chcesz. Można to zrobić na różne sposoby.

Oto przykład jak tego użyć:

#include <functional>
#include <memory>
#include <stdio.h>
#include <list>
#include <map>

#include "Events.hpp"
#include "Events.cpp"

using namespace std; 

class DisplayTextEvent : public IEventData {
public:
    DECLARE_EVENT(DisplayTextEvent); 

    DisplayTextEvent(const string &text){
        mStr = text; 
    }
    ~DisplayTextEvent(){
        printf("Deleted event data\n"); 
    }
    const string &GetText(){
        return mStr; 
    }
private:
    string mStr; 
}; 

class Emitter { 
public:
    Emitter(shared_ptr<IEventManager> em){
        mEmgr = em; 
    }
    void EmitEvent(){
        mEmgr->QueueEvent(shared_ptr<IEventData>(
            new DisplayTextEvent("Hello World!"))); 
    }
private:
    shared_ptr<IEventManager> mEmgr; 
}; 

class Receiver : public EventListener{
public:
    Receiver(shared_ptr<IEventManager> em) : EventListener(em){
        mEmgr = em; 

        OnEvent<DisplayTextEvent>([&](shared_ptr<DisplayTextEvent> data){
            printf("It's working: %s\n", data->GetText().c_str()); 
        }); 
    }
    ~Receiver(){
        mEmgr->RemoveListener(DisplayTextEvent::ID(), std::bind(&Receiver::OnExampleEvent, this, placeholders::_1)); 
    }
    void OnExampleEvent(IEventDataPtr &data){
        auto ev = dynamic_pointer_cast<DisplayTextEvent>(data); 
        if(!ev) return; 
        printf("Received event: %s\n", ev->GetText().c_str()); 
    }
private:
    shared_ptr<IEventManager> mEmgr; 
}; 

int main(){
    auto emgr = shared_ptr<IEventManager>(new EventManager()); 


    Emitter emit(emgr); 
    {
        Receiver receive(emgr); 

        emit.EmitEvent(); 
        emgr->ProcessEvents(); 
    }
    emit.EmitEvent(); 
    emgr->ProcessEvents(); 
    emgr = 0; 

    return 0; 
}
 3
Author: Martin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-03-28 02:44:11

Bądź ostrożny z "systemem stylu wiadomości", prawdopodobnie zależy to od implementacji, ale zazwyczaj tracisz statyczne sprawdzanie typu, a następnie możesz bardzo utrudnić debugowanie niektórych błędów. Zauważ, że wywołanie metod obiektu jest już systemem podobnym do wiadomości.

Prawdopodobnie po prostu brakuje ci pewnych poziomów abstrakcji, na przykład do nawigacji gracz mógłby użyć nawigatora zamiast wiedzieć wszystko o samej mapie. Mówisz też, że this has usually descended into setting lots of pointers, co to za wskaźniki? Prawdopodobnie dajesz im złą abstrakcję?.. Bezpośrednie poznawanie obiektów o innych, bez przechodzenia przez interfejsy i półprodukty, jest prostą drogą do uzyskania ściśle powiązanego projektu.

 0
Author: Roman L,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-01-01 15:21:23

Wiadomości to zdecydowanie świetny sposób, ale systemy wiadomości mogą mieć wiele różnic. Jeśli chcesz, aby Twoje klasy były ładne i czyste, napisz je, aby były nieświadome systemu wiadomości i zamiast tego niech przyjmują zależności od czegoś prostego, takiego jak "ILocationService", które mogą być zaimplementowane w celu publikowania/żądania informacji od rzeczy takich jak Klasa Map. Podczas gdy skończysz z większą liczbą klas, będą one małe, proste i zachęcają do czystego projektowania.

Wiadomości to chodzi o coś więcej niż tylko odsprzęganie, ale także o bardziej asynchroniczną, współbieżną i reaktywną architekturę. Wzorce integracji przedsiębiorstw Gregor Hophe to świetna książka, która opowiada o dobrych wzorcach przesyłania wiadomości. Erlang OTP czy Scala implementacja wzorca aktora dostarczyły mi wielu wskazówek.

 0
Author: Alex Robson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-01-04 06:58:45

@ kellogs sugestia MVC jest poprawna i używana w kilku grach, choć jej dużo jest bardziej powszechna w aplikacjach internetowych i frameworkach. To może być przesada i za dużo na to.

Przemyślałbym Twój projekt, dlaczego gracz musi rozmawiać z wrogami? Czy oboje nie mogli dziedziczyć po klasie aktorskiej? Dlaczego aktorzy muszą rozmawiać z mapą?

Jak czytam to co napisałem to zaczyna pasować do frameworka MVC...Najwyraźniej ostatnio wykonałem zbyt dużo pracy z railami. Jednak byłbym chętni do postawienia, muszą tylko wiedzieć rzeczy, takie jak, zderzają się z innym aktorem, i mają pozycję, która powinna być w stosunku do mapy tak czy inaczej.

Oto implementacja Planetoid {[10] } nad którą pracowałem. Twoja gra może być i prawdopodobnie jest złożona.

 -1
Author: EnabrenTane,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-01-01 12:43:32