Jaka jest dobra praktyka projektowania, aby uniknąć pytania o typ podklasy?


11

Czytałem, że gdy twój program musi wiedzieć, jaką klasą jest obiekt, zwykle wskazuje na błąd projektowy, więc chcę wiedzieć, co jest dobrą praktyką, aby sobie z tym poradzić. Implementuję Kształt klasy z odziedziczonymi po nim różnymi podklasami, takimi jak Okrąg, Wielokąt lub Prostokąt, i mam różne algorytmy, aby wiedzieć, czy Okrąg koliduje z Wielokątem czy Prostokątem. Przypuśćmy, że mamy dwa wystąpienia Shape i chcemy wiedzieć, czy jedno z nich koliduje, w tej metodzie muszę wywnioskować, jaki typ podklasy to obiekt, który koliduję, aby wiedzieć, jaki algorytm powinienem wywołać, ale jest to zły projekt lub praktyka? Tak to rozwiązałem.

abstract class Shape {
  ShapeType getType();
  bool collide(Shape other);
}

class Circle : Shape {
  getType() { return Type.Circle; }

  bool collide(Shape other) {
    if(other.getType() == Type.Rect) {
      collideCircleRect(this, (Rect) other);     
    } else if(other.getType() == Type.Polygon) {
      collideCirclePolygon(this, (Polygon) other);
    }
  }
}

To zły wzór? Jak mogę rozwiązać ten problem bez konieczności wnioskowania o typach podklasy?


1
W rezultacie każda instancja, np. Koło, zna wszystkie inne typy kształtów. Więc wszystkie są w jakiś sposób połączone. A gdy tylko dodasz nowy kształt, taki jak Trójkąt, ostatecznie dodasz obsługę Trójkątów wszędzie. To zależy od tego, co chcesz zmieniać częściej, czy dodasz nowe Kształty, ten projekt jest zły. Ponieważ masz wiele rozwiązań - Twoje wsparcie dla trójkątów musi być dodane wszędzie. Zamiast tego należy wyodrębnić wykrywanie kolizji do osobnej klasy, która może współpracować ze wszystkimi typami i delegować.
pakujący


IMO sprowadza się to do wymagań wydajnościowych. Im bardziej szczegółowy kod, tym bardziej zoptymalizowany może być i tym szybciej będzie działał. W tym szczególnym przypadku (także zaimplementowanym) sprawdzanie typu jest prawidłowe, ponieważ dostosowane testy kolizji mogą być niesamowicie szybsze niż ogólne rozwiązanie. Ale gdy wydajność środowiska wykonawczego nie jest krytyczna, zawsze wybieram podejście ogólne / polimorficzne.
marstato,

Dzięki wszystkim w moim przypadku wydajność jest krytyczna i nie będę dodawać nowych kształtów, może wykonuję podejście CollisionDetection, jednak nadal musiałem znać typ podklasy, czy powinienem zachować metodę „Type getType ()” w Kształtować czy zamiast tego robić jakąś „instancję” za pomocą Shape w klasie CollisionDetection?
Alejandro

1
Nie ma skutecznej procedury kolizji między Shapeobiektami abstrakcyjnymi . Twoja logika zależy od wewnętrznych elementów innego obiektu, chyba że sprawdzasz kolizję punktów granicznych bool collide(x, y)(podzbiór punktów kontrolnych może być dobrym kompromisem). W przeciwnym razie musisz jakoś sprawdzić typ - jeśli naprawdę jest potrzeba abstrakcji, wówczas tworzenie Collisiontypów (dla obiektów w obszarze bieżącego aktora) powinno być właściwym podejściem.
dreszcz

Odpowiedzi:


13

Wielopostaciowość

Tak długo, jak używasz getType()lub coś podobnego, nie używasz polimorfizmu.

Rozumiem, że musisz wiedzieć, jaki masz typ. Ale każda praca, którą chciałbyś wykonać, wiedząc, że naprawdę powinna zostać zepchnięta na zajęcia. Następnie po prostu powiedz, kiedy to zrobić.

Kod proceduralny otrzymuje informacje, a następnie podejmuje decyzje. Kod zorientowany obiektowo każe obiektom robić rzeczy.
- Alec Sharp

Ta zasada nazywa się powiedz, nie pytaj . Postępowanie zgodnie z tym pomaga ci nie rozpowszechniać szczegółów takich jak pisanie i tworzenie logiki, która na nie działa. Dzięki temu klasa wywraca się na lewą stronę. Lepiej jest zachować to zachowanie w klasie, aby mogło się zmienić, gdy klasa się zmieni.

Kapsułkowanie

Możesz mi powiedzieć, że żadne inne kształty nigdy nie będą potrzebne, ale ja ci nie wierzę i ty też nie powinieneś.

Dobrym efektem następującego po enkapsulacji jest to, że łatwo jest dodawać nowe typy, ponieważ ich szczegóły nie rozprzestrzeniają się w kodzie, w którym pojawiają się ifi switchlogika. Kod nowego typu powinien znajdować się w jednym miejscu.

Nieświadomy typ systemu wykrywania kolizji

Pozwól, że pokażę ci, jak zaprojektowałbym skuteczny system wykrywania kolizji, który działa z dowolnym kształtem 2D, nie dbając o typ.

wprowadź opis zdjęcia tutaj

Powiedz, że miałeś to narysować. Wydaje się proste. To wszystko krąży. Kuszące jest stworzenie klasy kręgu, która rozumie kolizje. Problem polega na tym, że przesuwa nas myślenie, które rozpada się, gdy potrzebujemy 1000 kół.

Nie powinniśmy myśleć o kręgach. Powinniśmy myśleć o pikselach.

Co, jeśli powiem ci, że ten sam kod, którego używasz do rysowania tych facetów, jest tym, czego możesz użyć do wykrycia, kiedy się dotykają, a nawet tego, który użytkownik klika.

wprowadź opis zdjęcia tutaj

Tutaj narysowałem każdy okrąg unikalnym kolorem (jeśli twoje oczy są wystarczająco dobre, aby zobaczyć czarny kontur, po prostu zignoruj ​​to). Oznacza to, że każdy piksel tego ukrytego obrazu odwzorowuje z powrotem na to, co go narysował. Haszapa ładnie się tym zajmuje. W ten sposób możesz faktycznie robić polimorfizm.

Ten obraz, którego nigdy nie musisz pokazywać użytkownikowi. Tworzysz go za pomocą tego samego kodu, który narysował pierwszy. Tylko w różnych kolorach.

Gdy użytkownik kliknie koło, wiem dokładnie, które koło, ponieważ tylko jedno koło ma ten kolor.

Kiedy narysuję okrąg na innym, mogę szybko odczytać każdy piksel, który zamierzam zastąpić, zrzucając go do zestawu. Kiedy skończę, ustawiam punkty dla każdego koła, z którym się zderzyło, a teraz muszę tylko zadzwonić do każdego z nich, aby powiadomić go o kolizji.

Nowy typ: prostokąty

Wszystko to wykonano za pomocą kół, ale pytam: czy działałoby inaczej z prostokątami?

Żadna wiedza z kręgów nie wyciekła do systemu detekcji. Nie obchodzi go promień, obwód ani punkt środkowy. Dba o piksele i kolor.

Jedyną częścią tego systemu kolizji, który należy docisnąć do poszczególnych kształtów, jest unikalny kolor. Poza tym kształty mogą myśleć o rysowaniu kształtów. W każdym razie są w tym dobrzy.

Teraz, kiedy piszesz logikę kolizji, nie obchodzi cię, jaki masz podtyp. Mówisz mu, żeby się zderzył, i mówi ci, co znalazł pod kształtem, który udaje, że rysuje. Nie musisz znać typu. A to oznacza, że ​​możesz dodać tyle podtypów, ile chcesz, bez konieczności aktualizacji kodu w innych klasach.

Opcje realizacji

Naprawdę nie musi to być unikalny kolor. Mogą to być rzeczywiste odwołania do obiektów i zapisywać poziom pośredni. Ale te nie wyglądałyby tak ładnie, gdy zostały narysowane w tej odpowiedzi.

To tylko jeden przykład implementacji. Z pewnością są inni. To miało pokazać, że im bliżej pozwalasz tym podtypom kształtowym trzymać się ich pojedynczej odpowiedzialności, tym lepiej działa cały system. Prawdopodobnie istnieją szybsze i wymagające mniej pamięci rozwiązania, ale jeśli zmuszą mnie do rozpowszechnienia wiedzy na temat podtypów wokół, nie chciałbym ich używać nawet przy wzroście wydajności. Nie użyłbym ich, chyba że wyraźnie ich potrzebuję.

Podwójna wysyłka

Do tej pory całkowicie ignorowałem podwójną wysyłkę . Zrobiłem to, ponieważ mogłem. Tak długo, jak kolidująca logika nie dba o to, które dwa typy kolidowały, nie jest to potrzebne. Jeśli go nie potrzebujesz, nie używaj go. Jeśli uważasz, że możesz go potrzebować, odłóż radzenie sobie z nim tak długo, jak możesz. Takie podejście nazywa się YAGNI .

Jeśli zdecydujesz, że naprawdę potrzebujesz różnych rodzajów kolizji, zapytaj siebie, czy n podtypów kształtu naprawdę potrzebuje n 2 rodzajów kolizji. Do tej pory bardzo ciężko pracowałem, aby ułatwić dodanie innego podtypu kształtu. Nie chcę zepsuć go implementacją podwójnej wysyłki, która zmusza okręgi do rozpoznania istnienia kwadratów.

Ile jest rodzajów kolizji? Trochę spekulacji (niebezpieczna rzecz) wymyśla zderzenia sprężyste (sprężyste), nieelastyczne (lepkie), energetyczne (eksplodujące) i niszczące (szkodliwe). Może być ich więcej, ale jeśli jest to mniej niż n 2, nie przepuszczajmy naszych kolizji.

Oznacza to, że gdy moja torpeda trafi w coś, co przyjmuje obrażenia, nie musi WIEDZIEĆ, że uderzyła w statek kosmiczny. Musi tylko powiedzieć: „Ha ha! Odebrałeś 5 punktów obrażeń”.

Rzeczy, które zadają obrażenia, wysyłają komunikaty o uszkodzeniach do rzeczy, które przyjmują komunikaty o uszkodzeniach. W ten sposób możesz dodawać nowe kształty bez informowania innych o nowym kształcie. Ostatecznie rozprzestrzeniasz się tylko wokół nowych rodzajów kolizji.

Statek kosmiczny może odesłać z powrotem na torpę „Ha ha! Odebrałeś 100 punktów obrażeń”. a także „Utknąłeś teraz w moim kadłubie”. A torp może odesłać: „Cóż, skończyłem, więc zapomnij o mnie”.

W żadnym momencie nie wie dokładnie, co to jest. Po prostu wiedzą, jak ze sobą rozmawiać przez interfejs kolizyjny.

Teraz, na pewno, podwójna wysyłka pozwala kontrolować rzeczy bardziej ściśle niż to, ale czy naprawdę tego chcesz ?

Jeśli tak, pomyśl przynajmniej o podwójnej wysyłce poprzez abstrakcje, jakie rodzaje kolizji przyjmuje kształt, a nie o rzeczywistej implementacji kształtu. Ponadto zachowanie podczas kolizji jest czymś, co można wstrzyknąć jako zależność i przekazać do tej zależności.

Występ

Wydajność jest zawsze krytyczna. Ale to nie znaczy, że zawsze jest to problem. Testuj wydajność. Nie tylko spekuluj. Poświęcenie wszystkiego innego w imię wydajności zwykle nie prowadzi do wykonania kodu.



+1 za „Nie możesz mi powiedzieć, że inne kształty będą potrzebne, ale ja ci nie wierzę i nie powinieneś”.
Tulains Córdova,

Myślenie o pikselach nigdzie cię nie doprowadzi, jeśli ten program nie polega na rysowaniu kształtów, ale na obliczeniach czysto matematycznych. Ta odpowiedź oznacza, że ​​powinieneś poświęcić wszystko dla postrzeganej obiektowej czystości. Zawiera również sprzeczność: najpierw mówisz, że powinniśmy oprzeć cały nasz projekt na pomyśle, że w przyszłości będziemy potrzebować więcej rodzajów kształtów, a potem mówisz „YAGNI”. Wreszcie zaniedbujesz fakt, że ułatwienie dodawania typów często oznacza, że ​​trudniej jest dodawać operacje, co jest złe, jeśli hierarchia typów jest względnie stabilna, ale operacje bardzo się zmieniają.
Christian Hackl,

7

Opis problemu brzmi jak powinieneś używać Multimetod (aka Multiple dispatch), w tym konkretnym przypadku - Double dispatch . Pierwsza odpowiedź była długa na temat tego, jak ogólnie radzić sobie z kolidującymi kształtami w renderowaniu rastrowym, ale wierzę, że OP chciał rozwiązania „wektorowego”, a może cały problem został przeformułowany pod względem Shapes, co jest klasycznym przykładem w wyjaśnieniach OOP.

Nawet cytowany artykuł na Wikipedii używa tej samej metafory kolizji, pozwólcie, że przytoczę (Python nie ma wbudowanych multimetod, jak niektóre inne języki):

@multimethod(Asteroid, Asteroid)
def collide(a, b):
    """Behavior when asteroid hits asteroid"""
    # ...define new behavior...
@multimethod(Asteroid, Spaceship)
def collide(a, b):
    """Behavior when asteroid hits spaceship"""
    # ...define new behavior...
# ... define other multimethod rules ...

Kolejnym pytaniem jest, w jaki sposób uzyskać wsparcie dla wielu metod w swoim języku programowania.



Tak, do odpowiedzi dodano specjalny przypadek wysyłki wielokrotnej, zwanej też Multimethods
Roman Susi,

5

Ten problem wymaga przeprojektowania na dwóch poziomach.

Najpierw musisz wykreślić logikę wykrywania kolizji między kształtami a kształtami. Dzieje się tak, aby nie naruszać OCP za każdym razem, gdy trzeba dodać nowy kształt do modelu. Wyobraź sobie, że masz już zdefiniowany okrąg, kwadrat i prostokąt. Możesz to zrobić w ten sposób:

class ShapeCollisionDetector
{
    public void DetectCollisionCircleCircle(Circle firstCircle, Circle secondCircle)
    { 
        //Code that detects collision between two circles
    }

    public void DetectCollisionCircleSquare(Circle circle, Square square)
    {
        //Code that detects collision between circle and square
    }

    public void DetectCollisionCircleRectangle(Circle circle, Rectangle rectangle)
    {
        //Code that detects collision between circle and rectangle
    }

    public void DetectCollisionSquareSquare(Square firstSquare, Square secondSquare)
    {
        //Code that detects collision between two squares
    }

    public void DetectCollisionSquareRectangle(Square square, Rectangle rectangle)
    {
        //Code that detects collision between square and rectangle
    }

    public void DetectCollisionRectangleRectangle(Rectangle firstRectangle, Rectangle secondRectangle)
    { 
        //Code that detects collision between two rectangles
    }
}

Następnie musisz ustawić odpowiednią metodę wywoływania w zależności od kształtu, który ją wywołuje. Możesz to zrobić za pomocą polimorfizmu i Wzorca Odwiedzającego . Aby to osiągnąć, musimy mieć odpowiedni model obiektowy. Po pierwsze, wszystkie kształty muszą być zgodne z tym samym interfejsem:

    interface IShape
{
    void DetectCollision(IShape shape);
    void Accept (ShapeVisitor visitor);
}

Następnie musimy mieć nadrzędną klasę odwiedzających:

    abstract class ShapeVisitor
{
    protected ShapeCollisionDetector collisionDetector = new ShapeCollisionDetector();

    abstract public void VisitCircle (Circle circle);

    abstract public void VisitSquare(Square square);

    abstract public void VisitRectangle(Rectangle rectangle);

}

Używam tutaj klasy zamiast interfejsu, ponieważ każdy obiekt użytkownika musi mieć atrybut ShapeCollisionDetectortypu.

Każda implementacja IShapeinterfejsu tworzy instancję odpowiedniego gościa i wywołuje odpowiednią Acceptmetodę obiektu, z którym wywołuje obiekt, na przykład:

    class Circle : IShape
{
    public void DetectCollision(IShape shape)
    {
        CircleVisitor visitor = new CircleVisitor(this);
        shape.Accept(visitor);
    }

    public void Accept(ShapeVisitor visitor)
    {
        visitor.VisitCircle(this);
    }
}

    class Rectangle : IShape
{
    public void DetectCollision(IShape shape)
    {
        RectangleVisitor visitor = new RectangleVisitor(this);
        shape.Accept(visitor);
    }

    public void Accept(ShapeVisitor visitor)
    {
        visitor.VisitRectangle(this);
    }
}

A konkretni odwiedzający wyglądaliby tak:

    class CircleVisitor : ShapeVisitor
{
    private Circle Circle { get; set; }

    public CircleVisitor(Circle circle)
    {
        this.Circle = circle;
    }

    public override void VisitCircle(Circle circle)
    {
        collisionDetector.DetectCollisionCircleCircle(Circle, circle);
    }

    public override void VisitSquare(Square square)
    {
        collisionDetector.DetectCollisionCircleSquare(Circle, square);
    }

    public override void VisitRectangle(Rectangle rectangle)
    {
        collisionDetector.DetectCollisionCircleRectangle(Circle, rectangle);
    }
}

    class RectangleVisitor : ShapeVisitor
{
    private Rectangle Rectangle { get; set; }

    public RectangleVisitor(Rectangle rectangle)
    {
        this.Rectangle = rectangle;
    }

    public override void VisitCircle(Circle circle)
    {
        collisionDetector.DetectCollisionCircleRectangle(circle, Rectangle);
    }

    public override void VisitSquare(Square square)
    {
        collisionDetector.DetectCollisionSquareRectangle(square, Rectangle);
    }

    public override void VisitRectangle(Rectangle rectangle)
    {
        collisionDetector.DetectCollisionRectangleRectangle(Rectangle, rectangle);
    }
}

W ten sposób nie musisz zmieniać klas kształtów za każdym razem, gdy dodajesz nowy kształt, i nie musisz sprawdzać typu kształtu, aby wywołać odpowiednią metodę wykrywania kolizji.

Wadą tego rozwiązania jest to, że jeśli dodajesz nowy kształt, musisz rozszerzyć klasę ShapeVisitor o metodę dla tego kształtu (np. VisitTriangle(Triangle triangle)), A w konsekwencji musiałbyś zaimplementować tę metodę we wszystkich innych gościach. Ponieważ jednak jest to rozszerzenie, w tym sensie, że nie są zmieniane żadne istniejące metody, a tylko nowe są dodawane, nie narusza to OCP , a narzut kodu jest minimalny. Ponadto, korzystając z tej klasy ShapeCollisionDetector, unikniesz naruszenia SRP i unikniesz nadmiarowości kodu.


5

Podstawowym problemem jest to, że w większości współczesnych języków programowania OO przeciążenie funkcji nie działa z dynamicznym wiązaniem (tzn. Rodzaj argumentów funkcji jest określany w czasie kompilacji). Potrzebne byłoby wirtualne wywołanie metody, które jest wirtualne na dwóch obiektach, a nie tylko na jednym. Takie metody nazywane są wieloma metodami . Istnieją jednak sposoby naśladowania tego zachowania w językach takich jak Java, C ++ itp. W tym przypadku bardzo przydatna jest podwójna wysyłka .

Podstawową ideą jest to, że używasz polimorfizmu dwukrotnie. Kiedy zderzają się dwa kształty, można wywołać poprawną metodę zderzenia jednego z obiektów poprzez polimorfizm i przekazać drugi obiekt o ogólnym kształcie. W wywoływanej metodzie wiesz, czy ten obiekt jest kołem, prostokątem czy czymkolwiek. Następnie wywołujesz metodę kolizji na przekazanym obiekcie kształtu i przekazujesz mu ten obiekt. To drugie wywołanie ponownie znajduje właściwy typ obiektu poprzez polimorfizm.

abstract class Shape {
  bool collide(Shape other);
  bool collide(Rect other);
  bool collide(Circle other);
}

class Circle : Shape {

  bool collide(Shape other) {
    return other.collide(this);
  }

  bool collide(Rect other) {
    // algorithm to detect collision between Circle and Rect
  }

  // ...
}

class Rect : Shape {

  bool collide(Shape other) {
    return other.collide(this);
  }

  bool collide(Circle other) {
    // algorithm to detect collision between Circle and Rect
  }

  // ...
}

Dużą wadą tej techniki jest jednak to, że każda klasa w hierarchii musi wiedzieć o całym rodzeństwie. Powoduje to duże obciążenie konserwacyjne, jeśli nowy kształt zostanie dodany później.


2

Może nie jest to najlepszy sposób na rozwiązanie tego problemu

Zderzenie kształtu matematyczne jest szczególne dla kombinacji kształtów. Oznacza to, że liczba podprogramów, których będziesz potrzebować, jest kwadratem liczby kształtów obsługiwanych przez system. Zderzenia kształtów nie są tak naprawdę operacjami na kształtach, ale operacjami, które przyjmują kształty jako parametry.

Strategia przeciążenia operatora

Jeśli nie możesz uprościć podstawowego problemu matematycznego, zaleciłbym podejście przeciążające operatora. Coś jak:

 public final class ShapeOp 
 {
     static { ... }

     public static boolean collision( Shape s1, Shape s2 )  { ... }
     public static boolean collision( Point p1, Point p2 ) { ... }
     public static boolean collision( Point p1, Square s1 ) { ... }
     public static boolean collision( Point p1, Circle c1 ) { ... }
     public static boolean collision( Point p1, Line l1 ) { ... }
     public static boolean collision( Square s1, Point p2 ) { ... }
     public static boolean collision( Square s1, Square s2 ) { ... }
     public static boolean collision( Square s1, Circle c1 ) { ... }
     public static boolean collision( Square s1, Line l1 ) { ... }
     (...)

Na statycznym interializatorze użyłbym odbicia, aby stworzyć mapę metod implementacji dyspersji diynamonowej w metodzie kolizji ogólnej (Shape s1, Shape s2). Statyczny intializer może również posiadać logikę do wykrywania brakujących funkcji kolizyjnych i zgłaszania ich, odmawiając załadowania klasy.

Jest to podobne do przeciążenia operatora C ++. W C ++ przeciążenie operatora jest bardzo mylące, ponieważ masz ustalony zestaw symboli, które możesz przeciążyć. Jednak koncepcja jest bardzo interesująca i może być powielana za pomocą funkcji statycznych.

Powodem, dla którego używałbym tego podejścia, jest to, że kolizja nie jest operacją na obiekcie. Kolizja jest operacją zewnętrzną, która mówi pewną relację o dwóch dowolnych obiektach. Ponadto statyczny inicjalizator będzie mógł sprawdzić, czy brakuje mi funkcji kolizji.

Jeśli to możliwe, uprość swój problem matematyczny

Jak wspomniałem, liczba funkcji kolizji jest kwadratem liczby typów kształtów. Oznacza to, że w systemie z tylko 20 kształtami będziesz potrzebować 400 procedur, z 21 kształtami 441 i tak dalej. Nie jest to łatwo rozszerzalne.

Ale możesz uprościć swoją matematykę . Zamiast rozszerzać funkcję kolizji, możesz zrasteryzować lub triangulować każdy kształt. W ten sposób silnik kolizji nie musi być rozszerzalny. Kolizja, odległość, skrzyżowanie, scalanie i kilka innych funkcji będzie uniwersalnych.

Triangulować

Czy zauważyłeś, że większość pakietów i gier 3D trianguluje wszystko? To jedna z form uproszczenia matematyki. Dotyczy to również kształtów 2D. Polisy można triangulować. Okręgi i splajny mogą być zbliżone do poligonów.

Znowu ... będziesz miał jedną funkcję kolizji. Twoja klasa stanie się wtedy:

public class Shape 
{
    public Triangle[] triangulate();
}

A twoje operacje:

public final class ShapeOp
{
    public static boolean collision( Triangle[] shape1, Triangle[] shape2 )
}

Prostsze, prawda?

Rasteryzuj

Możesz zrasteryzować swój kształt, aby mieć pojedynczą funkcję kolizji.

Rasteryzacja może wydawać się radykalnym rozwiązaniem, ale może być niedroga i szybka, w zależności od tego, jak precyzyjne muszą być kolizje kształtu. Jeśli nie muszą być precyzyjne (jak w grze), możesz mieć mapy bitowe o niskiej rozdzielczości. Większość aplikacji nie wymaga absolutnej precyzji z matematyki.

Przybliżenia mogą być wystarczające. Przykładem może być superkomputer ANTON do symulacji biologii. Jej matematyka odrzuca wiele efektów kwantowych, które są trudne do obliczenia, a dotychczasowe symulacje są zgodne z eksperymentami przeprowadzonymi w prawdziwym świecie. Modele grafiki komputerowej PBR stosowane w silnikach gier i pakietach renderujących stanowią uproszczenia, które zmniejszają moc komputera potrzebną do renderowania każdej klatki. Nie jest fizycznie dokładny, ale jest wystarczająco blisko, aby przekonać gołym okiem.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.