Демонстрация объектно-ориентированного программирования и шаблонов проектирования с помощью диаграмм классов и кода Python с использованием классов RPG.
Введение и мотивация
Задолго до того, как я узнал о классах компьютерного программирования и о том, как они используются для объектно-ориентированного программирования (ООП), я знал о классах в RPG (ролевых играх). Ролевые игры основаны на актерской игре, и игрокам необходимо создать персонажа на основе выбора класса. Каждый класс имеет свои особенности, особые способности и навыки. Например, в игре на тему средневекового фэнтези распространенными классами являются Боец, Волшебник и Разбойник. Бойцы хорошо известны своими боевыми навыками, Волшебники — использованием магии, а разбойники — хитростью и универсальностью.
Однако персонажи с одним и тем же классом не одинаковы. Некоторые классы RPG имеют подклассы и множество возможностей внутри одного класса. Боец, например, может сосредоточиться на стрельбе из лука, боях на мечах, боях с двумя оружиями и многом другом. То же самое справедливо для большинства классов RPG. Настольные системы, такие как D&D и Pathfinder, хорошо известны тем, что разработали очень большие и иногда сложные системы для создания персонажа, основанного на различных классах, подклассах и развитии уровней.
В компьютерном программировании классы не менее важны, чем в ролевых играх. Многие языки программирования (например, Python, Java и Objective-C) являются объектно-ориентированными и используют классы в качестве основы для создания объектов. Точное определение объектно-ориентированного языка сложное и может вызвать споры, особенно когда речь идет о языке под названием JavaScript, который основан на прототипах, а не на классах. Кто-то определяет его как объектно-ориентированный, а кто-то нет.
Здесь я не собираюсь вести обсуждение того, что именно представляет собой объектно-ориентированное программирование, и не собираюсь слишком глубоко говорить о конкретных принципах, лежащих в его основе. Но если вам интересно узнать больше и узнать, почему ООП необходимо, я рекомендую эту статью, в которой рассказывается об этом, а также демонстрируется использование классов RPG. Для получения дополнительной информации о шаблонах проектирования я рекомендую этот веб-сайт, а для получения дополнительных реализаций примеров кода — этот репозиторий. Тем не менее, я покажу несколько действительно интересных примеров использования классов для создания объектов, используя идеи классов ООП, которые реализуют классы RPG, и шаблоны проектирования для связи нескольких классов в вымышленной системе. Существует множество игр и приложений с созданием персонажей на основе классов RPG, и они, вероятно, используют некоторые шаблоны дизайна в своем коде.
Эта другая статья дает введение в ООП в python и иллюстрирует разницу между классами и объектами с покемоном, объект (Пикачу) является экземпляром класса (покемон), а класс имеет атрибуты и методы:
Первый класс: Боец
Здесь я создаю класс Fighter с двумя подклассами closeFighter и rangedFighter. Чтобы создать класс, нам нужно инициализировать атрибуты, а затем определить методы. Слово self, которое стоит в начале функции, дает им доступ к другим переменным и функциям внутри класса.
Используя принцип ООП, называемый наследованием, класс Fighter имеет два подкласса (как называется в RPG) или дочерних классов (как называется в компьютерном программировании), называемых closeFighter и rangedFighter. Эти потомки имеют все атрибуты и методы родительского класса Fighter, но для каждого из них переписан метод extraAction. Это основано на принципе ООП, называемом полиморфизмом («много форм»), потому что метод extraAction имеет одно и то же имя в дочерних классах, но имеет другую форму, поскольку каждый подкласс применяет его по-своему.
На приведенной ниже диаграмме показана диаграмма классов UML. Стрелки представляют наследование, а метод extraActionn переписывается в каждом подклассе.
#creating class class Fighter: #initializing attributes def __init__(self, name, weapon='Weapon', armor='Armor'): self.name = name self.weapon = weapon self.armor = armor #defining methods def attack(self): print(self.name+' attacks using '+self.weapon) def defend(self): print(self.name+' defends with '+self.armor) def extraAction(self): print('Do something') #example of object created using Fighter class geralt = Fighter('Geralt') geralt.attack() geralt.defend() geralt.extraAction()
Geralt attacks using Weapon Geralt defends with Armor Do something
#creating subclass of Fighter - inheritance class closeFighter(Fighter): def __init__(self, name): Fighter.__init__(self,name=name, weapon='GreatSword', armor='HeavyArmor') def extraAction(self): print(self.name+' kicks the enemy') class rangedFighter(Fighter): def __init__(self, name): Fighter.__init__(self, name=name, weapon='Bow', armor='MediumArmor') def extraAction(self): print(self.name+' gets further away from the enemy') close = closeFighter('Close Fighter') close.attack() close.defend() close.extraAction() print() ranged = rangedFighter('Ranged Fighter') ranged.attack() ranged.defend() ranged.extraAction()
Close Fighter attacks using GreatSword Close Fighter defends with HeavyArmor Close Fighter kicks the enemy Ranged Fighter attacks using Bow Ranged Fighter defends with MediumArmor Ranged Fighter gets further away from the enemy
Прототип с классом Fighter
Метод прототипа можно использовать, чтобы вместо создания нового объекта из класса каждый раз, когда необходимо его использовать, был готов прототип этого объекта для использования. Этот метод снижает вычислительные затраты, так как проще использовать прототип, чем каждый раз создавать объект из класса. Интересным примером использования прототипов является язык программирования Javascript, известный как объектно-ориентированный язык, основанный на прототипах, поскольку он основан на кодировании прототипов и объектов, наследуемых от объектов.
В настольной ролевой игре один человек обычно является DM (мастером подземелий). Этот человек отвечает за создание задач и руководство другими игроками в игре. DM обычно управляет NPC (неигровыми персонажами), и у DM обычно есть прототипы персонажей, которых они могут использовать, вместо того, чтобы каждый раз создавать NPC. В видеоигре RPG действует та же концепция NPC, но обычно за ней стоит AI (искусственный интеллект), а не DM. В обоих случаях несколько NPC совершенно одинаковы, поэтому интересно использовать прототипы, а не создавать каждый раз новый класс.
# NPC prototype from copy import deepcopy class NPCs(): def __init__(self): #dictionary used for prototyping self.itens = { 'closeFighter_NPC': closeFighter('closeFighter_NPC'), 'rangedFighter_NPC': rangedFighter('rangedFighter_NPC') } #creating NPC - prototyping instead of creating a new class every time def create(self, item): return deepcopy(self.itens[item])
# prototype example - multiple fighters npcs = NPCs() close1 = npcs.create('closeFighter_NPC') close2 = npcs.create('closeFighter_NPC') close3 = npcs.create('closeFighter_NPC') ranged1 = npcs.create('rangedFighter_NPC') ranged2 = npcs.create('rangedFighter_NPC') ranged3 = npcs.create('rangedFighter_NPC') close1.attack() close2.attack() close3.attack() ranged1.attack() ranged2.attack() ranged3.attack()
closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword rangedFighter_NPC attacks using Bow rangedFighter_NPC attacks using Bow rangedFighter_NPC attacks using Bow
# prototype example - creating an army of 30 fighters of close range army = [] for _ in range(30): army.append(npcs.create('closeFighter_NPC')) #making every soldier attack for soldier in army: soldier.attack()
closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword closeFighter_NPC attacks using GreatSword
Стратегия для колдовских классов
Однако, если классу требуется слишком много методов, может быть интересно создать для этого стратегию. В этом контексте мы должны учитывать принцип единой ответственности, который гласит, что каждый класс должен нести ответственность за одну часть функциональности программы, чтобы классы программного обеспечения не становились слишком большими и слишком сложными для понимания. Шаблон проектирования Strategy — это интересный способ реализовать то, что было бы слишком сложно для одного класса.
У персонажей RPG есть способности, которые описывают, как они обладают определенной характеристикой, и их способности делают их более вероятными для выполнения определенных действий, чем другие. Например, человек с высокой харизмой обладает сильным характером и может легко влиять на других. С другой стороны, такие способности, как Мудрость и Интеллект, могут показаться похожими, но Мудрость измеряет силу воли, инстинкты и интуицию, а Интеллект измеряет рассуждения и память.
Игроки могут даже выполнять схожие действия, используя различные виды способностей в зависимости от их опыта и мотивации к действию. Одним из примеров этого является колдовство: друиды и клирики используют мудрость для произнесения заклинаний, волшебники используют интеллект, а паладины, колдуны и барды используют харизму. И в то время как колдуны, чернокнижники и волшебники имеют что-то, называемое фокусом тайной магии, чтобы направить силу тайных заклинаний, остальные классы должны использовать материалы, которые разрушаются при произнесении заклинания.
Таким образом, шаблон проектирования стратегии очень интересен для этого случая. У нас есть, как видно на изображении ниже, схема для этой системы. Классы Wizzard, Sorcerer и Druid наследуются от класса SpellCaster. Класс SpellCaster объединяет классы ArcaneFocus и Casting. Класс ArcaneFocus имеет два дочерних класса, hasArcaneFocus и noArcaneFocus, а класс Casting имеет три дочерних класса в соответствии с типами способностей, используемых для применения: Casting_Intelligence, Casting_Charisma и Casting_Wisdom.
## arcane focus class ArcaneFocus: def focus(self): raise NotImplemented class hasArcaneFocus(ArcaneFocus): def focus(self): print("This character can use it's arcane focus to cast a spell, no need for material spell components.") class noArcaneFocus(ArcaneFocus): def focus(self): print("This characater can't use an arcane focus to cast a spell, material spell components are needed.") ## casting class Casting: def spellcasting(self): raise NotImplemented class Casting_Intelligence(Casting): def spellcasting(self): print("This character's spellcasting ability comes from careful study "\ "of books and memorization of rituals to cast a spell, therefore their spellcasting ability is Intelligence.") class Casting_Charisma(Casting): def spellcasting(self): print("This carachter's spellcasting ability comes from the force of their personality and willpower, "\ "therefore their spellcasting ability is Charisma.") class Casting_Wisdom(Casting): def spellcasting(self): print("This character's spellcasting ability comes from their innate knowledge and connection with a diety or "\ "force of nature, therefore their spellcasting ability is Wisdom.") ## creating classes using strategy class SpellCaster: arcaneFocus = None casting = None def __repr__(self): raise NotImplementedError def spells(self): print("This character is a spellcaster.") def arcane_focus(self): self.arcaneFocus.focus() def spellcasting(self): self.casting.spellcasting() class Wizzard(SpellCaster): arcaneFocus = hasArcaneFocus() casting = Casting_Intelligence() def __repr__(self): return "This character is a Wizzard." class Sorcerer(SpellCaster): arcaneFocus = hasArcaneFocus() casting = Casting_Charisma() def __repr__(self): return "This character is a Sorcerer." class Druid(SpellCaster): arcaneFocus = noArcaneFocus() casting = Casting_Wisdom() def __repr__(self): return "This character is a Druid." ## test case for spellcaster in (Wizzard(), Sorcerer(), Druid()): print(spellcaster) spellcaster.spells() spellcaster.arcane_focus() spellcaster.spellcasting() print("")
This character is a Wizzard. This character is a spellcaster. This character can use it's arcane focus to cast a spell, no need for material spell components. This character's spellcasting ability comes from careful study of books and memorization of rituals to cast a spell, therefore their spellcasting ability is Intelligence. This character is a Sorcerer. This character is a spellcaster. This character can use it's arcane focus to cast a spell, no need for material spell components. This carachter's spellcasting ability comes from the force of their personality and willpower, therefore their spellcasting ability is Charisma. This character is a Druid. This character is a spellcaster. This characater can't use an arcane focus to cast a spell, material spell components are needed. This character's spellcasting ability comes from their innate knowledge and connection with a diety or force of nature, therefore their spellcasting ability is Wisdom.
Государственный шаблон дизайна
Одна вещь, которая делает ролевые игры более интересными, — это условия. Есть много заклинаний и действий, которые могут изменить состояние игрока. Персонаж может временно испугаться, ослепнуть или даже потерять сознание. В этих условиях действия, которые они могут выполнять, отличаются от действий, совершаемых в нормальном сознательном состоянии. Следовательно, здесь можно использовать шаблон проектирования State для представления условий игроков и легкого перехода от одного состояния к другому.
from abc import ABCMeta, abstractmethod class CharacterState: @abstractmethod def move(self): pass @abstractmethod def investigate(self): pass @abstractmethod def attack(self): pass class CharacterStateConscious(CharacterState): def move(self): print('Can move normally.') def investigate(self): print('Can investigate normally.') def attack(self): print('Can attack normally.') class CharacterStateUnconscious(CharacterState): def move(self): print("Can't move.") def investigate(self): print('Can investigate their dreams.') def attack(self): print("Can't attack.") class CharacterStateFrightened(CharacterState): def move(self): print("Can't move closer to the source of fear.") def investigate(self): print('Has disadvantage investigating while seeing their source of fear.') def attack(self): print('Has disadvantage attacking while seeing their source of fear.') class CharacterStateBlinded(CharacterState): def move(self): print('Can move guided by their senses other than sight.') def investigate(self): print('Can investigate guided by their senses other than sight.') def attack(self): print('Has disadvantage attacking.') class Character: _state = None def __init__(self, state: CharacterState): self.transition_to(state) def transition_to(self, state: CharacterState): self._state = state self._state.context = self def move(self): self._state.move() def investigate(self): self._state.investigate() def attack(self): self._state.attack() ## test case #starting character as conscious character = Character(CharacterStateConscious()) #conscious state print('\033[1m'+'Conscious state'+'\033[0m') character.move() character.investigate() character.attack() print() #unconscious state print('\033[1m'+'Unconscious state'+'\033[0m') character.transition_to(CharacterStateUnconscious()) character.move() character.investigate() character.attack() print() #frightened state print('\033[1m'+'Frightened state'+'\033[0m') character.transition_to(CharacterStateFrightened()) character.move() character.investigate() character.attack() print() #blinded state print('\033[1m'+'Blinded state'+'\033[0m') character.transition_to(CharacterStateBlinded()) character.move() character.investigate() character.attack()
Conscious state Can move normally. Can investigate normally. Can attack normally. Unconscious state Can't move. Can investigate their dreams. Can't attack. Frightened state Can't move closer to the source of fear. Has disadvantage investigating while seeing their source of fear. Has disadvantage attacking while seeing their source of fear. Blinded state Can move guided by their senses other than sight. Can investigate guided by their senses other than sight. Has disadvantage attacking.