March 7, 2023

אבסטרקציה פרקטית - שיעור מהניסיון

אבסרטקציה פרקטית

אבסטרקציה יוצרת גורם המממש רעיון.
כל מימוש נוסף שנגזר מהגורם יכול להוסיף התנהגות נוספת.
או לחילופין - אבסטרקציה בפרקטיקה מאגדת רעיונות שונים בתוך רעיון אחד.

מערכת לחימה במשחק תפקידים

לעצב מחלקות במשחקי תפקידים נותן לנו אפשרות להיות יצירתי במימוש רעיונות.

במשחק התפקידים שאנחנו בונים יש לדמויות שלנו ביגוד שמקנה להם נקודות הגנה והתקפה.

חרב - 20 נקודות התקפה
שריון - 15 נקודות הגנה
מגן - 10 נקודות הגנה

נתחיל ממערכת פשוטה,
לכל דמות יש נקודות התקפה ונקודות הגנה שמחושב מסכום החפצים שלהם.

נתחיל מכמה מחלקות מאוד פשוטות:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from dataclasses import dataclass


@dataclass
class Item:
Name: str


@dataclass
class EquipableItem:
Name: str
Attack: int
Defense: int

@dataclass
class CharacterEquips:
Armor: EquipableItem
WeaponOne: EquipableItem
WeaponTwo: EquipableItem

וכעת הפונקציה שתחשב את כמות הנזק:

1
2
3
4
5
6
7
def TotalDamage(first: EquipableItem, second: EquipableItem):
firstDef = first.Armor.Defense + first.WeaponOne.Defense + first.WeaponTwo.Defense
secondAttack = second.Armor.Attack + second.WeaponOne.Attack + second.WeaponTwo.Attack

totalDamage = secondAttack - firstDef
totalDamage = 0 if totalDamage <= 0 else totalDamage
return totalDamage

בשביל לחשב את הנזק שדמות אחת מבצעת על השנייה נוכל לחשב בעזרת הלבוש שלהם.

הבעיה המהותית היא שהנזק הוא לינארי וכל נזק שנוסיף ייגרום לתחרותיות קשה.

אפילו במספרים קטנים זה יהיה בעייתי כי אין לדמות החלשה סיכוי:

אני לא אתמקד בעיצוב המערכת של המשחק, אחת מדרכים לשבור את הגדילה הלינארית היא בעזרת הוספת אלמנטים נוספים למשחק.

כעת נרצה להוסיף אלמנט הגנתי למגנים - לכל מגן יהיה סיכוי כלשהו להגן לחלוטין מהנזק.
במשחקי תפקידים האפקט הזה נקרא “חסימה”.

המימוש הנאיבי

ללא אבסטרקציה אנו ניישם מיד את הרעיון העומד מאחורי החסימה:

1
2
3
4

@dataclass
class Shield(EquipableItem):
BlockChance: int

ונוסיף את קוד הבדיקה:

1
2
3
4
if isinstance(first.WeaponOne, Shield) and random.randint(0, 100) < first.WeaponOne.BlockChance:
return 0
if isinstance(first.WeaponTwo, Shield) and random.randint(0, 100) < first.WeaponTwo.BlockChance:
return 0

הרעיון אכן מומש, חוץ מזה ש-randint קשה לבדיקה וגם כי הכנסו קוד מאוד ספציפי למגנים לפונקציית ההתקפה שלנו.

הצורה הנאיבית מבחינת תכנות מונחה אובייקטים לוקח כל קונספט כמובן מאליו וישירות הופך את הרעיון למימוש:

הבעיה הפרקטית הראשונה במימוש ישיר הוא שנצטרך להוסיף לאותו מקום בקוד עוד ועוד לוגיקה כך שפונקצית חישוב הנזק תגדל ותגדל.

כדי למגר את הישירות הזו נוכל להשתמש באסטרקציה כי ליצור גורם מקשר בין רעיון החסימה למימוש מכניקת המשחק.

המימוש האבסטרקטי

שלב ראשון - ליצור אפקט לנזק

נוציא אבסטרקציה מרעיון החסימה.
לחסום נזק זה בעצם להוריד את הנזק ל-0.
הורדת הנזק זה בעצם שינוי הנזק שנעשה.
אם משנים את הנזק נוכל לעשות אפקטים שגם מעלים את הנזק או מורידים אותו בהתאם לההתנהגות שאנחנו רוצים.

נכתוב את זה בקוד:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@dataclass
class DamageEffect:
def Perform(self, damage, item):
pass

@dataclass
class Blockable(DamageEffect):
BlockChance: int

def Perform(self, damage, item):
if random.randint(0, 100) < self.BlockChance:
print("Blocked")
return 0
return damage

סתם לשעשוע, אם נרצה באמת אפקט שמחזק את החפץ נוכל לממש גם כן:

1
2
3
4
5
6
7
8
9
@dataclass
class DoubleFun(DamageEffect):
DoubleChance: int

def Perform(self, damage, item):
if random.randint(0, 100) < self.DoubleChance:
print("Doubled!")
return damage * 2
return damage

שלב שני -נוסיף אפקטים לחפצים

1
2
3
4
5
6
@dataclass
class EquipableItem:
Name: str
Attack: int
Defense: int
DamageEffect: DamageEffect = DamageEffect()

שלב שלישי - עדכון פונקציית הנזק בהתחשבות החפצים

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def TotalDamage(first: EquipableItem, second: EquipableItem):
firstDef = first.Armor.Defense + first.WeaponOne.Defense + first.WeaponTwo.Defense
secondAttack = second.Armor.Attack + \
second.WeaponOne.Attack + second.WeaponTwo.Attack

totalDamage = secondAttack - firstDef
totalDamage = 0 if totalDamage <= 0 else totalDamage

totalDamage = first.WeaponOne.DamageEffect.Perform(
totalDamage, first.WeaponOne)

totalDamage = first.WeaponTwo.DamageEffect.Perform(
totalDamage, first.WeaponOne)

return totalDamage

כעת הפונקציה מתחשבת באבסטרקציה במקום בחוק מוגדר מראש:

הכלה מול הורשה

נקודה נוספת לתשומת לבכם -
יש כאן שימושים בשני טכניקות, גם ההורשה וגם ההכלה.
ההורשה ממומשת בכך שכל אפקט יורש את מחלקת הבסיס - אם כי פה זה מוגדר יותר כמו ממשק מאשר מחלקה משלה.
ההוכלה אומרת שכל חפץ מכיל אפקט כזה, ולא הוא בעצמו אפקט כזה.
שימו לב יש לזה משקל רב - להאם רכיב א’ הוא סוג של רכיב ב’, או רכיב א’ מכיל רכיב’ ב’.
למשל:

  • סוזוקי הוא רכב
  • לרכב יש 4 גלגלים

Visitor pattern

בהעברת item ו-damage אנו בונים את רכיב האפקט כvisitor.
זאת אומרת שהוא אינו יודע פרט מעבר למה שהוא מקבל בפונקציה ובמקום שהוא יישתלט על פונקציה חישוב הנזק, הוא חלק ממנה ויודע רק את המעט.

זהו לא visitor חזק כל כך אבל פה הוא נמצא בשימוש גם כן.


סיכום

כמובן שמימוש מערכות משחקים זה עניין מורכב ותלוי במשחק ובאיזה סוג של כיף רוצים.
ככה גם בקוד שלנו לעיתים נרצה להוסיף רמת אבסטרקציה נוספת על מנת שנוכל לאגד כמה רעיונות תחתיו.

כאן במקום לממש באופן ישיר אפקט חסימה, נתנו למילה “אפקט” מימוש משלו בתור מחלקה.
יצירת מחלקות עבור קונספטים זה חלק מתכנות מונחה עצמים - כאשר עושים את זה טוב נוכל ליצור מערכת מודולרית שאינה תלויה ברכביה,
המימושים יהיו במחלקות שונות ככה שהקוד מבוזר,
ובמידה וצריך לשנות משהו אנו עומדים במנגנון האחריות הבודדת עבור כל רכיב במערכת.

אז לסיכום:

אבסטרקציה תורמת למערכת שלנו מודולריות
אבסטרקציה עוזרת לממש קונספטים באופן עקיף
אבסטרקציה מסתירה את המימוש מהקוד

תודה על הקריאה!

על הפוסט

הפוסט נכתב על ידי Ilya, רישיון על ידי CC BY-NC-ND 4.0.

שתפו את הפוסט

Email Facebook Linkedin Print

קנו לי קפה

#Software#Programming#Abstraction