Jak utworzyć kod dla wielu unikalnych broni / zaklęć / mocy

22

Jestem niedoświadczonym programistą tworzącym grę typu „roguelike” w stylu FTL , używając Pythona (jak dotąd brak PyGame, ponieważ nadal interesuję się tylko tekstem).

Moja gra będzie zawierać dużą liczbę broni (około 50 na początek), które dają unikalne umiejętności. Staram się zrozumieć, jak ustrukturyzować kod obiektowy w sposób, który jest zarówno potężny (pod względem umożliwienia broni mieć radykalnie różne efekty), jak i rozszerzalny (tak, że mogę łatwo dodać więcej broni później, np. Upuszczając ją do folderu ).

Moim pierwszym instynktem było posiadanie klasy BasicWeapon i dziedziczenie innej broni od tej klasy. Wydaje mi się to jednak problematyczne: albo muszę sprawić, by klasa BasicWeapon była tak oklepana, że ​​w zasadzie jest bezużyteczna (jedyne cechy wspólne wszystkich broni to nazwa i typ (pistolet, topór itp.)), Albo muszę przewidzieć każdy wyjątkowy efekt, jaki kiedykolwiek wymyślę i zakoduję w BasicWeapon.

Ten ostatni jest oczywiście niemożliwy, ale ten pierwszy można jeszcze wykorzystać. Pozostaje mi jednak pytanie: gdzie umieścić kod dla poszczególnych broni?

Czy mogę utworzyć plasmarifle.py, rocketlauncher.py, swarmofbees.py itp. Itd. I upuścić je wszystkie w folderze, z którego gra może je zaimportować?

A może istnieje sposób na utworzenie pliku w stylu bazy danych (może coś tak prostego jak arkusz kalkulacyjny Excel), który w jakiś sposób zawiera unikalny kod dla każdej broni - bez potrzeby uciekania się do eval / exec?

Jeśli chodzi o to ostatnie rozwiązanie (bazę danych), myślę, że podstawową kwestią, z którą walczę, jest to, że chociaż rozumiem, że pożądane jest utrzymanie separacji między kodem a danymi, wydaje mi się, że broń zaciera granicę między „kodem” i trochę „dane”; reprezentują ogromną różnorodność podobnych rzeczy, które można znaleźć w grze, w tym sensie, że są jak dane, ale większość z nich będzie wymagać co najmniej jakiegoś unikalnego kodu, który nie jest współdzielony z żadnym innym przedmiotem, w tym sensie są one oczywiście, kod.

Częściowe rozwiązanie, które znalazłem gdzie indziej na tej stronie, sugeruje nadanie klasie BasicWeapon kilku pustych metod - on_round_start (), on_attack (), on_move () itp. - a następnie zastąpienie tych metod dla każdej broni. W odpowiedniej fazie cyklu walki gra wywoła odpowiednią metodę dla broni każdej postaci, a tylko te, które mają zdefiniowane metody, faktycznie coś zrobią. To pomaga, ale wciąż nie mówi mi, gdzie muszę umieścić kod i / lub dane dla każdej broni.

Czy jest jakiś inny język lub narzędzie, którego mogę użyć jako pewnego rodzaju chimery typu half-data, half-code? Czy całkowicie rzeźnię dobrą praktykę programowania?

Moje rozumienie OOP jest co najwyżej szkicowe, więc doceniłbym odpowiedzi, które nie są zbyt informatyczne.

EDYCJA: Vaughan Hilts wyjaśnił w swoim poście poniżej, że zasadniczo mówię o programowaniu opartym na danych. Istota mojego pytania brzmi: w jaki sposób mogę wdrożyć projekt oparty na danych, aby dane mogły zawierać skrypty, umożliwiające nowym broniom robienie nowych rzeczy bez zmiany kodu głównego programu?

henrebotha
źródło
@ Byte56 Podobne; ale myślę, że tego właśnie OP próbuje uniknąć. Myślę, że starają się znaleźć podejście oparte na danych. Popraw mnie, jeśli się mylę.
Vaughan Hilts,
Zgadzam się, że starają się znaleźć podejście bardziej zorientowane na dane. Szczególnie podoba mi się odpowiedź Josha na to pytanie: gamedev.stackexchange.com/a/17286/7191
MichaelHouse
Przepraszam za to. :) Mam zły nawyk czytania „zaakceptowanej odpowiedzi”.
Vaughan Hilts,

Odpowiedzi:

17

Chcesz podejścia opartego na danych prawie na pewno, chyba że twoja gra będzie całkowicie nieoczekiwana i / lub wygenerowana proceduralnie w rdzeniu.

Zasadniczo obejmuje to przechowywanie informacji o twojej broni w wybranym języku znaczników lub formacie pliku. Zarówno XML, jak i JSON są dobrymi, czytelnymi opcjami, których można użyć, aby edycja była dość prosta, bez potrzeby korzystania ze skomplikowanych edytorów, jeśli tylko chcesz szybko zacząć. ( A Python może także bardzo łatwo analizować XML! ) Ustawiłbyś atrybuty takie jak „moc”, „obrona”, „koszt” i „statystyki”, które są wszystkie istotne. Sposób, w jaki uporządkujesz swoje dane, zależy od Ciebie.

Jeśli broń wymaga dodania efektu statusu, nadaj mu węzeł efektu statusu, a następnie określ efekty efektu statusu za pośrednictwem innego obiektu opartego na danych. Dzięki temu Twój kod będzie mniej zależny od konkretnej gry, a edytowanie i testowanie gry będzie banalne. Dodatkową zaletą jest to, że nie trzeba przez cały czas kompilować.

Dodatkowa lektura jest dostępna poniżej:

Vaughan Hilts
źródło
2
Coś jak system oparty na komponentach, w którym komponenty są odczytywane za pomocą skryptów. W ten sposób: gamedev.stackexchange.com/questions/33453/…
MichaelHouse
2
I kiedy już to robisz, ustaw skrypt jako część tych danych, aby nowa broń mogła robić nowe rzeczy bez głównych zmian w kodzie.
Patrick Hughes,
@Vaughan Hilts: dziękuję, oparte na danych wydaje się dokładnie tym, czego intuicyjnie zrozumiałem, że potrzebuję. Pozostawiam pytanie dłużej otwarte, ponieważ wciąż potrzebuję odpowiedzi, ale prawdopodobnie wybiorę to jako najlepszą odpowiedź.
henrebotha
@Patrick Hughes: właśnie tego chcę! W jaki sposób mogę to zrobić? Czy możesz mi pokazać prosty przykład lub samouczek?
henrebotha
1
Najpierw potrzebujesz silnika skryptowego w swoim silniku, wiele osób wybiera LUA, który ma dostęp do systemów rozgrywki, takich jak efekty i statystyki. Następnie, ponieważ odtwarzasz już swoje obiekty z opisu danych, możesz osadzić skrypt wywoływany przez silnik za każdym razem, gdy nowy obiekt zostanie aktywowany. W dawnych czasach MUD nazywano to „proc” (skrót od Process). Trudność polega na tym, aby funkcje gry w silniku były wystarczająco elastyczne, aby można je było wywoływać z zewnątrz, oraz z wystarczającą liczbą funkcji.
Patrick Hughes,
6

(Przepraszam, że podałem odpowiedź zamiast komentarza, ale nie mam jeszcze przedstawiciela).

Odpowiedź Vaughana jest świetna, ale chciałbym dodać moje dwa centy.

Jednym z głównych powodów, dla których chcesz używać XML lub JSON i parsować je w środowisku wykonawczym, jest zmiana i eksperymentowanie z nowymi wartościami bez konieczności ponownej kompilacji kodu. Ponieważ Python jest interpretowany i, moim zdaniem, dość czytelny, możesz mieć surowe dane w pliku ze słownikiem i wszystko zorganizowane:

weapons = {
           'megaLazer' : {
                          'name' : "Mega Lazer XPTO"
                          'damage' : 100
                       },
           'ultraCannon' : {
                          'name' : "Ultra Awesome Cannon",
                          'damage' : 200
                       }
          }

W ten sposób po prostu importujesz plik / moduł i używasz go jako zwykłego słownika.

Jeśli chcesz dodać skrypty, możesz skorzystać z dynamicznej natury funkcji Pythona i 1. klasy. Możesz zrobić coś takiego:

def special_shot():
    ...

weapons = { 'megalazer' : { ......
                            shoot_gun = special_shot
                          }
          }

Chociaż uważam, że byłoby to sprzeczne z projektowaniem opartym na danych. Aby uzyskać 100% DDD, będziesz mieć informacje (dane) określające, jakie funkcje i kod będą używane przez określoną broń. W ten sposób nie łamiesz DDD, ponieważ nie łączysz danych z funkcjonalnością.

Vasco Correia
źródło
Dziękuję Ci. Widok prostego przykładu kodu pomógł mu kliknąć.
henrebotha
1
+1 za miłą odpowiedź i wystarczająco dużo powtórzeń, aby móc komentować. ;) Witamy.
wer
4

Projektowanie oparte na danych

Niedawno przesłałem coś takiego do recenzji kodu .

Po kilku sugestiach i ulepszeniach powstał prosty kod, który zapewniłby względną elastyczność w tworzeniu broni w oparciu o słownik (lub JSON). Dane są interpretowane w czasie wykonywania, a Weaponsama weryfikacja przeprowadzana jest przez samą klasę, bez konieczności polegania na całym interprecie skryptu.

Projektowanie oparte na danych, mimo że Python jest językiem interpretowanym (zarówno pliki źródłowe, jak i pliki danych można edytować bez potrzeby ich ponownej kompilacji), wydaje się być właściwą rzeczą w takich przypadkach, jak ten, który przedstawiłeś. To pytanie zawiera więcej szczegółów na temat koncepcji, jej zalet i wad. Jest też ładna prezentacja na Cornell University na ten temat.

W porównaniu z innymi językami, takimi jak C ++, które prawdopodobnie używałyby języka skryptowego (takiego jak LUA) do obsługi danych i interakcji silnika x ogólnie ze skryptami oraz określonego formatu danych (takiego jak XML) do przechowywania danych, Python może faktycznie wszystko samo w sobie (biorąc pod uwagę standard, dictale także weakreften ostatni, szczególnie w przypadku ładowania zasobów i buforowania).

Niezależny programista może jednak nie stosować podejścia ekstremalnego opartego na danych, jak sugerowano w tym artykule :

Ile jestem na temat projektowania opartego na danych? Nie sądzę, aby silnik gry zawierał jeden wiersz kodu specyficznego dla gry. Niejeden. Brak zakodowanych rodzajów broni. Brak zakodowanego układu interfejsu. Brak zakodowanej jednostki AI. Nada. Zamek błyskawiczny. Zilch.

Być może dzięki Pythonowi można skorzystać z najlepszego podejścia zarówno obiektowego, jak i opartego na danych, dążącego zarówno do produktywności, jak i rozszerzalności.

Proste przetwarzanie próbek

W konkretnym przypadku omawianym podczas przeglądu kodu słownik przechowywałby zarówno „atrybuty statyczne”, jak i logikę do interpretacji - gdyby broń zachowywała się w sposób warunkowy.

Na poniższym przykładzie miecz powinien mieć pewne umiejętności i statystyki w rękach postaci klasy „antypadykady” i nie mieć żadnych efektów, z niższymi statystykami, gdy są używane przez inne postacie):

WEAPONS = {
    "bastard's sting": {
        # magic enhancement, weight, value, dmg, and other attributes would go here.
        "magic": 2,

        # Those lists would contain the name of effects the weapon provides by default.
        # They are empty because, in this example, the effects are only available in a
        # specific condition.    
        "on_turn_actions": [],
        "on_hit_actions": [],
        "on_equip": [
            {
                "type": "check",
                "condition": {
                    'object': 'owner',
                    'attribute': 'char_class',
                    'value': "antipaladin"
                },
                True: [
                    {
                        "type": "action",
                        "action": "add_to",
                        "args": {
                            "category": "on_hit",
                            "actions": ["unholy"]
                        }
                    },
                    {
                        "type": "action",
                        "action": "add_to",
                        "args": {
                            "category": "on_turn",
                            "actions": ["unholy aurea"]
                        }
                    },
                    {
                        "type": "action",
                        "action": "set_attribute",
                        "args": {
                            "field": "magic",
                            "value": 5
                        }
                    }
                ],
                False: [
                    {
                        "type": "action",
                        "action": "set_attribute",
                        "args": {
                            "field": "magic",
                            "value": 2
                        }
                    }
                ]
            }
        ],
        "on_unequip": [
            {
                "type": "action",
                "action": "remove_from",
                "args": {
                    "category": "on_hit",
                    "actions": ["unholy"]
                },
            },
            {
                "type": "action",
                "action": "remove_from",
                "args": {
                    "category": "on_turn",
                    "actions": ["unholy aurea"]
                },
            },
            {
                "type": "action",
                "action": "set_attribute",
                "args": ["magic", 2]
            }
        ]
    }
}

Do celów testowych stworzyłem proste Playeri Weaponklasy: pierwszą do trzymania / wyposażania broni (w ten sposób nazywając jej warunkowe ustawienie on_equip), a drugą jako pojedynczą klasę, która pobierałaby dane ze słownika, na podstawie nazwy przedmiotu przekazywanej jako argument podczas Weaponinicjalizacji. Nie odzwierciedlają prawidłowego projektu klas gier, ale nadal mogą być przydatne do testowania danych:

class Player:
    """Represent the player character."""

    inventory = []

    def __init__(self, char_class):
        """For this example, we just store the class on the instance."""
        self.char_class = char_class

    def pick_up(self, item):
        """Pick an object, put in inventory, set its owner."""
        self.inventory.append(item)
        item.owner = self


class Weapon:
    """A type of item that can be equipped/used to attack."""

    equipped = False
    action_lists = {
        "on_hit": "on_hit_actions",
        "on_turn": "on_turn_actions",
    }

    def __init__(self, template):
        """Set the parameters based on a template."""
        self.__dict__.update(WEAPONS[template])

    def toggle_equip(self):
        """Set item status and call its equip/unequip functions."""
        if self.equipped:
            self.equipped = False
            actions = self.on_unequip
        else:
            self.equipped = True
            actions = self.on_equip

        for action in actions:
            if action['type'] == "check":
                self.check(action)
            elif action['type'] == "action":
                self.action(action)

    def check(self, dic):
        """Check a condition and call an action according to it."""
        obj = getattr(self, dic['condition']['object'])
        compared_att = getattr(obj, dic['condition']['attribute'])
        value = dic['condition']['value']
        result = compared_att == value

        self.action(*dic[result])

    def action(self, *dicts):
        """Perform action with args, both specified on dicts."""
        for dic in dicts:
            act = getattr(self, dic['action'])
            args = dic['args']
            if isinstance(args, list):
                act(*args)
            elif isinstance(args, dict):
                act(**args)

    def set_attribute(self, field, value):
        """Set the specified field with the given value."""
        setattr(self, field, value)

    def add_to(self, category, actions):
        """Add one or more actions to the category's list."""
        action_list = getattr(self, self.action_lists[category])

        for action in actions:
            if action not in action_list:
                action_list.append(action)

    def remove_from(self, category, actions):
        """Remove one or more actions from the category's list."""
        action_list = getattr(self, self.action_lists[category])

        for action in actions:
            if action in action_list:
                action_list.remove(action)

Z pewnymi przyszłymi ulepszeniami, mam nadzieję, że pozwoli mi to nawet kiedyś mieć dynamiczny system rzemieślniczy, przetwarzający komponenty broni zamiast całej broni ...

Test

  1. Postać A wybiera broń, wyposaża ją (drukujemy jej statystyki), a następnie upuszcza;
  2. Postać B wybiera tę samą broń, wyposaż ją (i ponownie wydrukujemy jej statystyki, aby pokazać, jak się różnią).

Lubię to:

def test():
    """A simple test.

    Item features should be printed differently for each player.
    """
    weapon = Weapon("bastard's sting")
    player1 = Player("bard")
    player1.pick_up(weapon)
    weapon.toggle_equip()
    print("Enhancement: {}, Hit effects: {}, Other effects: {}".format(
        weapon.magic, weapon.on_hit_actions, weapon.on_turn_actions))
    weapon.toggle_equip()

    player2 = Player("antipaladin")
    player2.pick_up(weapon)
    weapon.toggle_equip()
    print("Enhancement: {}, Hit effects: {}, Other effects: {}".format(
        weapon.magic, weapon.on_hit_actions, weapon.on_turn_actions))

if __name__ == '__main__':
    test()

Powinien wydrukować:

Dla barda

Ulepszenie: 2, Efekty trafienia: [], Inne efekty: []

Dla antypaladyny

Ulepszenie: 5, Efekty działania: [„bezbożny”], Inne efekty: [„bezbożny aurea”]

Lucas Siqueira
źródło