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 Weapon
sama 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, dict
ale także weakref
ten 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 Player
i Weapon
klasy: 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 Weapon
inicjalizacji. 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
- Postać A wybiera broń, wyposaża ją (drukujemy jej statystyki), a następnie upuszcza;
- 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”]