[PL] XNA 3.0 – tworzymy demo graficzne - 0x01

Jak widać po tytule, zanosi sie na kolejny cykl artykułów. Jak pojawią się kolejne to postaram się dorobić sensowny spis treści.

Aby zacząć pisać demo takie jak chociażby wymienione w tym wpisie, wypadało by się przyjrzeć możliwościom Xna. Zakładam, że wszystko jest już poinstalowane i próbowaliście się chwilkę pobawić.

Napewno zwróciliście uwagę na specyficzny pipeline i model aplikacyjny jaki Xna oferuje.
Jest trochę odmienny od tego, który oferuje DirectX zarówno natywnie jak i MDX.

Podstawową klasą jaką mamy jest Game, po niej dziedziczymy własny twór, który jest spoiwem całej naszej wesołej i artystycznej produkcji.
W skrócie Game zawiera parę kluczowych dla nas elementów w postaci trzech metod: Initialize, Update oraz Draw.
Nazwy są oczywiste, initialize odpowiada za inicjalizację naszych zasobów i ustawień/parametrów związanych z naszą aplikacją (wg. naszych potrzeb i widzimisie).
Update odpowiada za aktualizację parametrów podczas wykonywania na poziomie każdej poszczególnej ramki/klatki. Tutaj zapewne będziemy chcieli wrzucić obsługę klawiatury, zaktualizować AI, poprzesuwać kamerę, świat i nasze obiekty. Draw odpowiada za rendering poszczególnej klatki. Tutaj co tu dodać, ostra jatka po ekranie.
Update i Draw mają taki sam parametr wejściowy GameTime, który odmierza upływ czasu i jest krytyczny przy odpowiedniej synchronizacji tego co się dzieje na ekranie.
Jeśli nic nie zmienialiśmy Update/Draw powinny się optymalizować do 60Hz (60 odświeżeń na sekundę). Jeśli nie chcemy takiego twardego ustawienia to zawsze możemy je zmienić via Game.IsFixedTimeStep oraz Game.TargetElapsedTime. Na Game się na razie zatrzymam i przejdę do kolejnej ważnej klasy a mianowicie GameComponent lub pochodna DrawableGameComponent. Te klasy zamykają w sobie pojedynczy komponent gry. Każdy z nich zawiera podobnie jak Game trzy bardzo istotne metody jak Initialize/Update oraz Draw (dostępne tylko w DrawableGameComponent).

Jak widać dzięki takiej strukturze nie musimy wszystkiego pchać w Game tylko skonstruować sobie w miarę sprytną hierarchię własnych obiektów, które będziemy chcieli docelowo wykorzystać w naszej grze lub to do czego ja zmierzam – demie. Wartość z GameComponent nie byłaby wielka gdybyśmy nie mieli mechanizmu zarządzania naszymi komponentami. Takowy istnieje poprzez Game.Components gdzie możemy nasze instancje komponentów dodawać i XNa już samo zarządzi procesem ich wgrywania/zwalniania/aktualizacji i rysowania.

Jedyne parametry, które później dla nas będą istotne to (Drawable)GameComponent.Enabled (uruchamia wykonywanie metody Update), oraz DrawableGameComponent.Visible (uruchamia wykonywanie metody Draw).

W zasadzie zatrzymując się tutaj mamy już prawie komplet wiedzy, aby zbudować sobie szkielet silnika do odgrywania dem. Przecież jeśli spojrzycie na tę produkcję to, co ona zawiera mogę opisać poniżej: 
* Odgrywaną w tle muzykę
* Listę “efektów graficznych”, które jeden po drugim się uruchamiają
* Część z tych efektów nich napewno to wiele efektów uruchomionych na raz kompozytowo złożonych z dodatkiem postprocessingu (czy przez składanie różnych rendertargetów z blendingiem czy via pixel shader)

Ubierając to w ten sposób, tak jak wspomniałem, abstrahując od konkretnej wiedzy i umiejętności jak poszczególne efekty napisać, silnik, który złoży nam to wszystko do kupy w jedno demo spróbowałem od strony architektonicznej ubrać w taki twór:

    public class DemoEffectComponent : DrawableGameComponent
    {
        private TimeSpan effectStart;
        private TimeSpan effectEnds;

        public int DemoEffectIndex;        

        public TimeSpan EffectStart
        {
            get
            {
                return effectStart;
            }
        }

        public TimeSpan EffectEnds
        {
            get
            {
                return effectEnds;
            }
        }

        protected DemoEngineGame DemoEngine
   {
            get
            {
                return (DemoEngineGame)Game;
            }
        }

        protected TimeSpan GetDemoTimer()
        {
            return DemoEngine.DemoTimer;
        }

        public DemoEffectComponent(Game game, TimeSpan start, TimeSpan end) :
            base(game)
        {
            effectStart = start;
            effectEnds = end;
        }

    }

    enum DemoStatus {
        Initialized,
        Playing,
        Paused,
        Stopped
    };

   public class DemoEngineGame : Game
    {
        private int currentEffectIndex = 0;
        private TimeSpan demoTimer = TimeSpan.Zero;
        private DemoStatus demoStatus = DemoStatus.Initialized;

        public TimeSpan DemoTimer
        {
            get
            {
                return demoTimer;
            }
        }

        public void AddDemoEffects(DemoEffectComponent[] demoEffects)
        {
            Components.Clear();
            int idx = 0;
            foreach (DemoEffectComponent gc in demoEffects)
            {
                gc.DemoEffectIndex = idx;
                gc.Enabled = false;
                gc.Visible = false;
                Components.Add(gc);
                idx++;
            }
        }

        public void ResetDemo()
        {
            ChangeEffect(0);
        }

        public void PlayDemo()
        {
            demoStatus = DemoStatus.Playing;
        }

        public void PauseDemo()
        {
            demoStatus = DemoStatus.Paused;
        }

        public void StopDemo()
        {
            demoStatus = DemoStatus.Stopped;
        }

        public void ChangeEffect(int idx)
        {
            if (idx >= 0)
            {
                currentEffectIndex = idx;
    if (currentEffectIndex == Components.Count)
                {
                    ExitDemo();
                }
            }
        }

        public void ChangeEffect()
        {
            ChangeEffect(currentEffectIndex + 1);
        }

        protected void ExitDemo()
        {
            Exit();
        }

        TimeSpan lastFrame = TimeSpan.Zero;

        protected override void Update(GameTime gameTime)
        {
            if (Keyboard.GetState().IsKeyDown(Keys.Escape))
        ExitDemo();
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.ExitDemo();

            Keys[] keysPressed = AdvancedKeyboard.GetKeysPressed();
            if (keysPressed.Count(key => key == Keys.Space)>0)
            {
                if (demoStatus == DemoStatus.Playing)
                    demoStatus = DemoStatus.Paused;
                else
                    demoStatus = DemoStatus.Playing;
            }

            if (keysPressed.Count(key => key == Keys.Right) > 0)
            {
                if (demoStatus == DemoStatus.Playing)
                {
                    ChangeEffect();
                    DemoEffectComponent c = (DemoEffectComponent)Components[currentEffectIndex];
                    demoTimer = c.EffectStart;
                }
            }

            if (keysPressed.Count(key => key == Keys.Left) > 0)
            {
                if (demoStatus == DemoStatus.Playing)
                {
                    ChangeEffect(currentEffectIndex-1);
                    DemoEffectComponent c = (DemoEffectComponent)Components[currentEffectIndex];
                    demoTimer = c.EffectStart;
                }
            }

            if (demoStatus == DemoStatus.Playing)
            {
                if (lastFrame == TimeSpan.Zero)
                {
                    lastFrame = gameTime.TotalRealTime;
                }
                demoTimer+=(gameTime.TotalRealTime - lastFrame);
                IEnumerator<IGameComponent> demoEffects = Components.GetEnumerator();

                int activeEffects = 0;
                while (demoEffects.MoveNext())
                {
                    DemoEffectComponent effect = (DemoEffectComponent)demoEffects.Current;

                    bool valid = effect.EffectStart <= demoTimer;
                    valid = valid & effect.EffectEnds > demoTimer;
                    if (valid)
                    {
                        activeEffects++;
                        effect.Enabled = true;
                        effect.Visible = true;
                    }
                    else
                    {
                        effect.Enabled = false;
                        effect.Visible = false;
                    }
                }

                if (activeEffects == 0)
                    ExitDemo();

                Window.Title = "Demo time: " + demoTimer.ToString();

                base.Update(gameTime);
            }
            else if (demoStatus == DemoStatus.Stopped)
            {
                ExitDemo();
            }
            lastFrame = gameTime.TotalRealTime; 
        }
    }

Mam nadzieję, że kod jest przejrzysty. Nie jest doskonały, ponieważ jeszcze nie zaimplementowałbym w nim paru przypadków (w ogóle nie uwzględnia playbacku muzyki, zwłaszcza przy przewijaniu), natomiast to co on nam realizuje:
* DemoEngineGame bazujący na Game pozwala nam na:
   - automatyzację pipeline’u efektów wraz z timeline’em poszczególnych z nich.
   - pozwala na przeskakiwanie pomiędzy poszczególnymi efektami (w przód i w tył)
   - pozwala na pause/resume odgywania dema
* DemoEffectComponent bazujący na DrawableGameComponent jedyne co dodaje to obsługę ramek czasowych od kiedy do kiedy nasz efekt ma działać oraz eksponuje lokalnie parę metod z DemoEngineGame. Do wypełnienia standardowo Initialize/Update/Draw. W dwóch ostatnich trzeba pamiętać, że aby poprawnie uwzględnić zdarzenia Pauzy/Resume oraz przewijania trzeba zignorować standardowy parametr GameTime i wykorzystać przekazywany z DemoEffectComponent.DemoTimer;

Przykładowe wykorzystanie przeze mnie polegało na stworzeniu paru efektów na bazie DemoEffectComponent i jedyne co potem musiałem zrobić to w Initialize mojej klasy z faktycznym demem musiałem dodać taki kod:

 public class SampleDemo : DemoEngineGame
    {
        DemoEffectComponent[] demoComponents;

        public SampleDemo()
        {
     graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        protected override void Initialize()
        {
            demoComponents = new DemoEffectComponent[] {
                new Effect_Thorus(this, TimeSpan.Zero,
new TimeSpan(0,0,0,10,0)),
                new Effect_GlowingSparks(this,
new TimeSpan(0,0,0,10,1),
new TimeSpan(0,0,0,20,0)),
                new Effect_TwistedBall(this,
new TimeSpan(0,0,0,12,0),
                         new TimeSpan(0,0,0,15,0))
            };
            this.AddDemoEffects(demoComponents);            
            base.Initialize();

            this.PlayDemo();
        }
}

Jak przyuważycie powyżej wgrywane są 3 efekty “Thorus, GlowingSparks oraz TwistedBall.
Timing jest Torus (0, 10 sekunda), GlowingSparks (10 sekunda, 20 sekunda), TwistedBall (12 sekunda, 15 sekunda).

Ciekawe jest, że czasy w TwistedBall oraz GlowingSparks pokrywają się ze sobą przez parę sekund, to oznacza, że oba na raz będą wyświetlane i składane (via Xna).

Technorati Tags: Polish posts,coding,gamedev,Xna