Kategorie
.net c# xna

XNA: Podstawy fizyki z JigLibX

podstawy-jiglibxSamo XNA, nie oferuje nam klas pomocnych w obliczeniach związanych z fizyką. Co prawda istnieje możliwość wykrywania samych kolizji, ale to jak zachowają się obiekty po jej wykryciu, zależy już tylko od napisanego przez nas kodu. Chyba, że skorzystamy z gotowych bibliotek do tego służących. Na tapetę wziąłem najpopularniejszą XNA’ową bibliotekę OpenSource – JigLibX.

Co można osiągnąć?

Powołując się na filmy na stronie autorów, 2 przykłady:

…oraz przykład stworzony przez samych autorów JigLibX:


Więcej filmów na stronie projektu.

Utworzenie projektu

Pierwsze co należy zrobić, to pobrać z sekcji Download, najnowsze źródła JigLibX’a. Po rozpakowaniu ich, tworzymy nowy projekt (w moim przypadku projekt XNA 3.1), klikamy w Solution Explorerze na główny korzeń naszego projektu (Solution NazwaNaszegoProjektu) prawym przyciskiem myszy i wybieramy Add › Existing project,

jiglibx-dodawanie-projektu1
Dodawanie istniejącego projektu do już istniejącego

następnie wybieramy z folderu gdzie rozpakowaliśmy wcześniej pobrany silnik fizyki: JigLibX › JigLibX.csproj.

jiglibx-dodawanie-projektu
Dodawanie istniejącego projektu do już istniejącego

Jeżeli wybraliśmy przy tworzeniu projektu, projekt w dla XNA w wersji 3.1, to należy teraz zrobić upgrade projektu JigLibX do wersji 3.1. Robimy to klikając prawym przyciskiem myszy, na korzeniu projektu JigLibX › Upgrade solution. Musimy to zrobić, aby później móc dodać w projekcie głównym referencje do projektu JigLibX’a. Inaczej nie będzie to możliwe, ponieważ obecnie jest on przeznaczony dla wersji 3.0. Zmiany w wersji 3.1 nie są na tyle duże jednak, aby w pełni poprawnie biblioteka ta działała pod tą wersją XNA.

Następnie, klikamy prawym przyciskiem myszy na korzeń naszego głównego projektu (w moim przypadku nazywa się on FizykaZJigLibX) › Add reference › Projects › JigLibX. W drzewie naszego projektu, w folderze references powinien pojawić się JigLibX. Od teraz mamy dostęp do tej przestrzeni nazw.

Dodanie systemu fizyki

Po pierwsze, konieczną rzeczą będzie dodanie odpowiednich przestrzeni nazw w pliku Game1.cs.

using JigLibX.Physics;
using JigLibX.Geometry;
using JigLibX.Collision;

Możemy zatem korzystać z klas napisanych przez twórców JigLibX’a. Na początek, w konstruktorze naszej klasy, stworzymy dwa obiekty.

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

PhysicsSystem world = new PhysicsSystem();
world.CollisionSystem = new CollisionSystemSAP();
}

Obiekt world, będący instancją klasy PhysicSystem, zainicjuje wszystkie niezbędne zmienne i będzie niejako kontrolerem zdarzeń zachodzących wewnątrz naszej aplikacji. Zdarzeń związanych z fizyką. W kolejnej linii, do właściwości CollisionSystem, przypisujemy utworzony obiekt typu CollisionSystemSAP, który jest konieczny, aby wykrywać wszelkie kolizje. Klasa ta wykorzystuje algorytm „sweep and prune” do wykrywania kolizji, który w tym przykładzie, będzie wystarczający, jeśli jednak zaglądniemy do folderu, Collision w projekcie JigLibX, zobaczymy, że mamy możliwość skorzystać z innych gotowych klas, wykorzystujących inne algorytmy, np. bazujący na siatce (grid). Jeżeli pominięto by ten krok, nie moglibyśmy reagować na kolizje, a obiekty po prostu przechodziły by przez siebie, bez żadnej reakcji.

Należy zrobić jeszcze jedną rzecz, mianowicie, regularnie aktualizować nasz obiekt w którym przechowywana jest logika fizyki. Aby to zrobić, wystarczy w metodzie Update, dodać następujący kod:

float timeStep = (float)gameTime.ElapsedGameTime.Ticks / TimeSpan.TicksPerSecond;
PhysicsSystem.CurrentPhysicsSystem.Integrate(timeStep);

Ponieważ PhysicsSystem.CurrentPhysicsSystem jest składową statyczną, jest ona dostępna z każdego miejsca w naszym projekcie, co niewątpliwie zwiększa wygodę użytkowania tego rozwiązania.

Utworzenie klasy aktora

Klasa aktora, będzie tak naprawdę służyła do utworzenia obiektu, w którym ustawiane będą odpowiednie parametry określające obiekt w naszej aplikacji, a które to są niezbędne do wszelkich obliczeń przy kolizjach z otoczeniem.

Utwórzmy zatem klasę BoxActor.cs, klikając na korzeń naszego projektu › Add › Class..

jiglibx-boxactor-class
Tworzenie klasy BoxActor.cs

Umieśćmy poniższy kod wewnątrz pliku BoxActor.cs:

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using JigLibX.Math;
using JigLibX.Physics;
using JigLibX.Geometry;
using JigLibX.Collision;

namespace FizykaZJigLibX
{
    public class BoxActor : DrawableGameComponent
    {
        private Vector3 position;
        private Vector3 scale;

        private Body _body;
        public Body Body
        {
            get
            {
                return _body;
            }
        }

        private CollisionSkin _skin;
        public CollisionSkin Skin
        {
            get
            {
                return _skin;
            }
        }

        public BoxActor(Game game, Vector3 position, Vector3 scale) : base(game)
        {
            this.position = position;
            this.scale = scale;
        }
    }
}

Klasa BoxActor dziedziczy po klasie DrawableGameComponent, ponieważ aktor musi być rysowany na ekranie, abyśmy mogli zobaczyć co się dzieje. Pole position przechowuje informacje o centralnym punkcie wewnątrz box’a, natomiast scale – jego wymiary. Body należy rozumieć, jako zmienną która przechowuje informację o ruchu oraz obracaniu się naszego obiektu. Skin odpowiada za wykrywanie kolizji.

Utworzenie Body i CollisionSkin

Kolejnym krokiem będzie dodanie na końcu konstruktora klasy BoxActor, następującego kodu:

_body = new Body();
_skin = new CollisionSkin(_body);
_body.CollisionSkin = _skin;

Do zmiennej _body, przypiszemy instancję klasy Body, która będzie przechowywać podstawowe informację o stanie w jakim znajduje się obiekt. _skin będzie posiadał informację o „powłoce” naszego obiektu, która zostanie utworzona na bazie, wcześniej ustawionego _body. Na koniec, konieczne jest jeszcze ustawienie własności CollisionSkin dla _body.

Teraz, zdecydujemy jaki kształt, będzie posiadał nasz aktor. Od tego zależy jak będzie się zachowywał na scenie. Służą do tego klasy Primitives znajdujące się w JigLibX › Primitives.

W naszym przypadku, zależy nam, aby nasz obiekt zachowywał się jak skrzynia, zatem wykorzystamy do tego klasę Box, dodając poniższy kod, na końcu konstruktora klasy BoxActor.

Box box = new Box(Vector3.Zero, Matrix.Identity, scale);
_skin.AddPrimitive(box, new MaterialProperties(0.8f, 0.8f, 0.7f));

W powyższym kodzie, utworzyliśmy nowy obiekt klasy Box, którego parametry w konstruktorze oznaczają kolejno: Pozycję środka naszego Box’a, orientację Box’a (Macierz jednostkowa oznacza brak rotacji itp.), oraz skalę, która ustalona będzie przy konstruowaniu obiektu BoxActor.

Druga linia, dodaje powierzchnię kolizyjną do naszego obiektu, aby mógł reagować z innymi obiektami. Pierwszy parametr, to typ obiektu z jakim mamy do czynienia, drugi parametr określa zachowanie się materiału (tarcie oraz elastyczność).

Ustalanie masy obiektu

Aby ustalić masę obiektu, której w dodatku środek ciężkości, będzie się zmieniał w zależności od kierunku sił jakie będą na ów obiekt oddziaływać, autorzy JigLibX, stworzyli taką oto metodę, którą dodamy do klasy BoxActor.

private Vector3 SetMass(float mass)
{
    PrimitiveProperties primitiveProperties = new PrimitiveProperties(
        PrimitiveProperties.MassDistributionEnum.Solid,
        PrimitiveProperties.MassTypeEnum.Mass, mass);

    float junk;
    Vector3 com;
    Matrix it;
    Matrix itCoM;

    Skin.GetMassProperties(primitiveProperties, out junk, out com, out it, out itCoM);

    Body.BodyInertia = itCoM;
    Body.Mass = junk;

    return com;
}

Na końcu konstruktora, tej samej klasy, ustalimy masę w następujący sposób:

Vector3 com = SetMass(1.0f);

I to wszystko, jeżeli chodzi o przejmowanie się masą naszego obiektu :). Oczywiście nic nie stoi na przeszkodzie, aby w trakcie działania aplikacji, dowolnie zmieniać masę obiektu.

Ustawianie Box’a w odpowiednim miejscu

W tym kroku, ustawimy nasz Box na pozycji, którą podawać będziemy w konstruktorze, w trakcie tworzenia naszego aktora. Jest to niezbędne, aby dopasować ciało naszego obiektu, jak i powierzchnię kolizyjną odpowiednio do samego obiektu, aby efekt był adekwatny i realistyczny.

Na końcu konstruktora BoxActor, dodajemy następujący kod:

_body.MoveTo(position, Matrix.Identity);
_skin.ApplyLocalTransform(new Transform(-com, Matrix.Identity));
_body.EnableBody();

Pierwsza linia przenosi „ciało” naszego aktora na określoną pozycję. Drugi parametr w metodzie MoveTo, oznacza orientację obiektu.

Druga linia, ustawia powierzchnię kolizyjną odpowiednio względem środka ciężkości naszego obiektu.

Ostatnia linia, włącza symulację dla _body i zarazem aktora. Ta linia jest niezbędna, ponieważ symulacja, jest standardowo wyłączona.

Tworzenie Świata.

Wszystkie ustawienia związane z framework’iem, zostały ustalone, zatem można przejść do tworzenia Świata. Na końcu konstruktora, w głównym pliku naszego projektu game1.cs, dodajmy następujący kod:

fallingBox = new BoxActor(this, new Vector3(5, 20, 5), new Vector3(2, 2, 2));
immovableBox = new BoxActor(this, new Vector3(0, -5, 0), new Vector3(10, 10, 10));
immovableBox.Body.Immovable = true;
Components.Add(fallingBox);
Components.Add(immovableBox);

Brakuje jeszcze składowych, które należy dodać nad wszystkimi metodami w klasie Game1:

BoxActor fallingBox;
BoxActor immovableBox;

Pierwsze dwie linie tworzą naszych aktorów. Parametry w konstruktorze, oznaczają kolejno: pierwszy – instancja klasy, w której utworzyliśmy aktora, w tym przypadku, klasa główna naszej aplikacji Game1, drugi – pozycja, trzeci parametr oznacza rozmiar elementu.

Trzecia linia oznacza, iż drugi obiekt będzie nieruchomy, a jak widać w parametrach przekazanych do konstruktora, będzie znacznie większy (5x), niż pierwszy obiekt.

Czwarta i piąta linia doda utworzone przez nas obiekty, do kolekcji komponentów, które automatycznie będą rysowane przez klasę Game, po której dziedziczy nasza aplikacja.

Rysowanie

Zajmiemy się teraz ostatnim etapem, czyli wyrzuceniem rezultatu na ekran, a właściwie ładnie to ujmując, narysowaniem naszej sceny. Najpierw jednak, musimy dodać model skrzynki, który wykorzystamy w naszym projekcie. Przykładowy model pochodzący z Innovative Games, można pobrać tutaj.

Po rozpakowaniu pliku i przekopiowaniu dwóch znajdujących się w nim plików (z modelem i teksturą), należy w Solution Explorerze, kliknąć prawym przyciskiem myszy na folder Content (w naszym projekcie) i wybrać Add › Existing item… oraz wybrać plik z modelem .fbx oraz powtórzyć krok, wybierając plik z teksturą.

jiglibx-content
Folder Content po dodaniu modelu i tekstury

Następnie w klasie BoxActor, utwórzmy nową metodę, która załaduje nasz model:

protected override void LoadContent()
{
    model = Game.Content.Load("boxModel");
}

oraz dodajmy nad wszystkimi metodami w tej klasie, deklarację zmiennej model:

private Model model;

Następnie dodajmy do tej samej klasy, następującą metodę:

private Matrix GetWorldMatrix()
{
    return
        Matrix.CreateScale(scale) *
        _skin.GetPrimitiveLocal(0).Transform.Orientation *
        _body.Orientation *
        Matrix.CreateTranslation(_body.Position);
}

Zwraca ona macierz Świata naszego obiektu, która otrzymujemy w wyniku przemnożenia kolejno macierzy: skali, powierzchni kolizyjnej, a tak naprawdę kąta w jakim się znajduje, „ciała” obiektu (również kąt), oraz pozycji „ciała” obiektu.

Potrzebne będą jeszcze macierze projekcji oraz widoku, które dodamy do klasy głównej aplikacji (w pliku game1.cs):

private Matrix _view;
public Matrix View
{
    get
    {
        return _view;
    }
}

private Matrix _projection;
public Matrix Projection
{
    get
    {
        return _projection;
    }
}

W konstruktorze klasy Game1, na końcu ustawmy macierz projekcji, dodając następującą linię:

_projection = Matrix.CreatePerspectiveFieldOfView(
    MathHelper.ToRadians(45.0f),
    (float)graphics.PreferredBackBufferWidth / (float)graphics.PreferredBackBufferHeight,
    0.1f,
    1000.0f
);

Nie będę opisywał parametrów macierzy projekcji, bo zakładam, że na tym etapie są one dość jasne, jednak jeśli nie, zapraszam do tego artykułu na msdn’ie.

Pozostało nam ustawić jeszcze macierz widoku, zrobimy to w metodzie Update, w pliku Game1.cs, dodając następującą linię kodu:

_view = Matrix.CreateLookAt(
    new Vector3(0, 10, 20),
    fallingBox.Body.Position,
    Vector3.Up
);

Linia ta spowoduje, że w centrum ekranu, znajdować się będzie nasz spadający obiekt. W trakcie spadania, zobaczymy jak, dojdzie do kolizji z obiektem, który został unieruchomiony.

Na koniec, pozostało nam jeszcze dodać metodę Draw dla BoxActor, która zostanie automatycznie wywołana dla tego obiektu przy rysowaniu.

public override void Draw(GameTime gameTime)
{
    BasicWorldGame game = (BasicWorldGame)Game;

    Matrix[] transforms = new Matrix[model.Bones.Count];
    model.CopyAbsoluteBoneTransformsTo(transforms);

    Matrix worldMatrix = GetWorldMatrix();

    foreach (ModelMesh mesh in model.Meshes)
    {
        foreach (BasicEffect effect in mesh.Effects)
        {
            effect.EnableDefaultLighting();
            effect.PreferPerPixelLighting = true;
            effect.World = transforms[mesh.ParentBone.Index] * worldMatrix;
            effect.View = game.View;
            effect.Projection = game.Projection;
        }
        mesh.Draw();
    }
}

Dzięki pierwszej lini, będziemy mieli dostęp do macierzy widoku i projekcji, tworząc referencję do obiektu głównego naszej aplikacji. Reszta kodu, jest typowa dla renderowania obiektów z użyciem klasy BasicEffect. Jedyną istotną rzeczą jest używanie jako macierzy Świata, macierzy którą obliczamy na bieżąco, a właściwie JigLibX oblicza za nas.

Artykuł bazuje na dokumentacji znalezionej tutaj, jednak wiele wskazuje na to, iż przytoczony tutaj tekst jest już nieaktualny, i napisana przeze mnie wersja powinna działać lepiej. Być może spowodowane to jest niepełną kompatybilnością z XNA 3.1, ale z tego co przyglądałem się metodom, jedna z nich, została ograniczona z 3 parametrów do dwóch. Wersja JigLibX 0.3.1 (stan na 11 Września 2009).

Gotowy projekt do pobrania (XNA 3.1) (2,1MB)

Skompilowane demo (XNA 3.1) (0,3MB)

Be Sociable, Share!

2 odpowiedzi na “XNA: Podstawy fizyki z JigLibX”

Możliwość komentowania jest wyłączona.