Klasy i Obiekty w C# – Dziedziczenie i Polimorfizm

Klasy i Obiekty w C# – Dziedziczenie i Polimorfizm w Grze RPG [Część 3]

Klasy i Obiekty w C# – Dziedziczenie i Polimorfizm

🧱 1. Problem z kopiowaniem kodu – czyli jak NIE programować

Kiedy próbujemy dodać nową klasę Potwór identyczną z Bohater, szybko zaczynamy… kopiować.
Ten sam konstruktor, te same metody Atakuj(), OtrzymajObrazenia() i PokazStatus().

To droga donikąd. A co jeśli masz 10 typów postaci?
Utrzymanie tego kodu będzie piekłem. Pora użyć dziedziczenia!

🌳 2. Klasa bazowa Postac – fundament każdej istoty

namespace DevHobby.Code.RPG;

public abstract class Postac
{
    // Właściwości auto-implemented (skrócona składnia)
    public string Imie { get; protected set; }
    public int PunktyZycia { get; protected set; }
    public int MaxPunktyZycia { get; protected set; } // Maksymalne HP dla każdej postaci
    public int Sila { get; protected set; }

    // Konstruktor
    public Postac(string imie, int punktyZycia, int sila)
    {
        Imie = imie;
        MaxPunktyZycia = punktyZycia > 0 ? punktyZycia : 1;
        PunktyZycia = MaxPunktyZycia;
        Sila = Math.Clamp(sila, 1, 100);  // Ogranicza wartość do zakresu
    }

    // Metoda ataku
    public virtual void Atakuj(Postac cel)
    {
        if (PunktyZycia <= 0)
        {
            Console.WriteLine($"{Imie} nie może atakować - jest pokonany!");
            return;
        }

        if (cel.PunktyZycia <= 0)
        {
            Console.WriteLine($"{cel.Imie} jest już pokonany!");
            return;
        }
    }

    // Metoda otrzymywania obrażeń
    protected internal void OtrzymajObrazenia(int obrazenia)
    {
        PunktyZycia -= obrazenia;

        if (PunktyZycia < 0) PunktyZycia = 0;

        Console.WriteLine($"   {Imie} ma teraz {PunktyZycia}/{MaxPunktyZycia} HP");

        if (PunktyZycia == 0)       
            Console.WriteLine($"💀 {Imie} został pokonany!");
    }

    // Metoda wyświetlania statusu
    public abstract void PokazStatus();
}

📌 Co warto zapamiętać:

  • abstract = nie można tworzyć instancji klasy Postac
  • virtual = metoda może zostać nadpisana
  • protected = dostęp tylko dla klas dziedziczących
  • internal = dostęp w ramach projektu

🦸‍♂️ 3. Klasa Bohater i jej dzieci – Wojownik, Łucznik, Mag

Każdy bohater dziedziczy z Postac, ale ma swoje supermoce.

namespace DevHobby.Code.RPG;

public abstract class Bohater : Postac
{
    // Właściwości auto-implemented
    public int Poziom { get; protected set; } = 1;

    // Konstruktor
    public Bohater(string imie, int punktyZycia, int sila) : base(imie, punktyZycia, sila) {}

    // Metoda leczenia
    public void Lecz(int punkty)
    {
        if (PunktyZycia == 0)
        {
            Console.WriteLine($"{Imie} nie może się leczyć - jest pokonany!");
            return;
        }

        int stareHP = PunktyZycia;
        PunktyZycia = Math.Min(PunktyZycia + punkty, MaxPunktyZycia);

        Console.WriteLine($"✨ {Imie} leczy się o {PunktyZycia - stareHP} HP!);
        Console.WriteLine($"   Aktualne HP: {PunktyZycia})");
    }

    // Metoda wyświetlania statusu
    public override void PokazStatus()
    {
        string pasekHP = new string('█', PunktyZycia / 10) +
                        new string('░', (100 - PunktyZycia) / 10);

        Console.WriteLine($"\n=== {Imie} - nasz Bohater ===");
        Console.WriteLine($"HP: [{pasekHP}] {PunktyZycia}/{MaxPunktyZycia}");
        Console.WriteLine($"Siła: {Sila} | Poziom: {Poziom}");
    }
}

💥 Wojownik.cs

namespace DevHobby.Code.RPG;

public class Wojownik : Bohater
{
    public Wojownik(string imie) : base(imie, 100, 30) {}

    public override void Atakuj(Postac cel)
    {
        base.Atakuj(cel);

        Console.WriteLine($"\n ⚔️ Wojownik {Imie} uderza z furią {cel.Imie}!");
        int obrazenia = (int)(Sila * (0.8 + new Random().NextDouble() * 0.4));
        Console.WriteLine($"   Zadaje {obrazenia} obrażeń!");
        cel.OtrzymajObrazenia(obrazenia);
    }
}

🏹 Lucznik.cs

public class Lucznik : Bohater
{
    public Lucznik(string imie) : base(imie, 90, 20) {}

    public override void Atakuj(Postac cel)
    {
        base.Atakuj(cel);

        Console.WriteLine($"\n 🏹 Lucznik {Imie} strzela celnie w {cel.Imie}!");
        int obrazenia = (int)(Sila * 1.2) + new Random().Next(1, 5); // Łucznik ma bonus do siły, ale mniejszy losowy modyfikator
        Console.WriteLine($"   Zadaje {obrazenia} obrażeń!");
        cel.OtrzymajObrazenia(obrazenia);
    }
}

Mag.cs

public class Mag : Bohater
{
    public Mag(string imie) : base(imie, 80, 40) {}

    public override void Atakuj(Postac cel)
    {
        base.Atakuj(cel);

        Console.WriteLine($"\n ✨ Mag {Imie} rzuca potężne zaklęcie na {cel.Imie}!");
        int obrazenia = (int)(Sila * 1.5); // Mag zadaje stałe, duże obrażenia
        Console.WriteLine($"   Zadaje {obrazenia} obrażeń!");
        cel.OtrzymajObrazenia(obrazenia);
    }
}

Zobacz też: Tworzenie klas w C# – część 1

👹 4. Klasa Potwor i bestie z piekła rodem

Potwory też dziedziczą z Postac, ale nie mają funkcji leczenia – bo kto widział Orka z apteczką?

namespace DevHobby.Code.RPG;

public abstract class Potwor : Postac
{
    public Potwor(string imie, int punktyZycia, int sila) : base(imie, punktyZycia, sila) {}

    public override void PokazStatus()
    {
        string pasekHP = new string('█', PunktyZycia / 10) +
                        new string('░', (100 - PunktyZycia) / 10);

        Console.WriteLine($"\n=== {Imie} - Potwor ===");
        Console.WriteLine($"HP: [{pasekHP}] {PunktyZycia}/100");
        Console.WriteLine($"Siła: {Sila}");
    }
}

🧟 Ork.cs, Goblin.cs, Smok.cs – unikalne ataki

Każdy potwór nadpisuje Atakuj() i prezentuje inny styl:

  • Ork – brutalny cios
  • Goblin – atak trucizną
  • Smok – zieje ogniem i niszczy wszystko na drodze

🧟 Ork.cs

namespace DevHobby.Code.RPG;

public class Ork : Potwor
{
    public Ork() : base("Ork", 80, 20) {}

    public override void Atakuj(Postac cel)
    {
        base.Atakuj(cel);

        Console.WriteLine($"\n 👺 {Imie} uderza maczugą w {cel.Imie}!");
        int obrazenia = Sila + 3; // Ork zadaje obrażenia większe o stałą wartość
        Console.WriteLine($"   Zadaje {obrazenia} obrażeń!");
        cel.OtrzymajObrazenia(obrazenia);
    }
}

🧟 Goblin.cs

namespace DevHobby.Code.RPG;

public class Goblin : Potwor
{
    public Goblin() : base("Goblin", 60, 15) {}

    public override void Atakuj(Postac cel)
    {
        base.Atakuj(cel);

        Console.WriteLine($"\n 🐍 {Imie} rzuca zatruty sztylet w {cel.Imie}!");
        int obrazenia = (int)(Sila * 0.9); // Goblin zadaje mniej obrażeń
        Console.WriteLine($"   Zadaje {obrazenia} obrażeń!");
        cel.OtrzymajObrazenia(obrazenia);
    }
}

🧟 Smok.cs

namespace DevHobby.Code.RPG;

public class Smok : Potwor
{
    public Smok() : base("Smok", 100, 30) {}

    public override void Atakuj(Postac cel)
    {
        base.Atakuj(cel);

        Console.WriteLine($"\n 🔥 {Imie} zieje ogniem na {cel.Imie}!");
        int obrazenia = Sila * 2; // Smok jest bardzo potężny i zadaje podwójne obrażenia
        Console.WriteLine($"   Zadaje {obrazenia} obrażeń!");
        cel.OtrzymajObrazenia(obrazenia);
    }
}

🧠 5. Polimorfizm – jedna metoda, wiele zachowań

Dzięki polimorfizmowi możemy stworzyć listę:

List<Postac> postacie = new() { nowyWojownik, goblin, mag, smok };

I wywołać:

foreach(var p in postacie) p.Atakuj(inny);

Każdy obiekt zachowa się inaczej, mimo że wywołujemy tę samą metodę Atakuj()!
To magia programowania obiektowego.


⚔️ 6. Arena Walk – przykład w Program.cs

Zobacz fragment prawdziwej walki:

using DevHobby.Code.RPG;

public class Program
{
    static void Main()
    {
        Console.WriteLine("Nacisnij Enter aby zobaczyć Bohaterów Gry");
        Console.ReadLine();
        Console.WriteLine("=== ARENA WALK ===\n");
        
        // Tworzenie bohaterów
        var rycerz = new Wojownik("Sir Galahad");
        var czarnoksieznik = new Mag("Mroczny Mag");
        var lucznik = new Lucznik("Legolas");

        var ork = new Ork();
        var smok = new Smok();
        var goblin = new Goblin();

        // Lista wszystkich postaci biorących udział w walce
        var wszystkiePostacie = new List<Postac> { rycerz, czarnoksieznik, lucznik, ork, goblin, smok };

        // Pokazujemy status przed walką
        wszystkiePostacie.ForEach(p => p.PokazStatus());

        Console.WriteLine("Nacisnij Enter aby rozpocząć Grę");
        Console.ReadLine();
        Console.WriteLine("\n--- WALKA ROZPOCZĘTA! ---");
        var random = new Random();

        // Symulacja walki
        while (wszystkiePostacie.Count(p => p.PunktyZycia > 0) > 1)
        {
            // Losowanie atakującego spośród żywych postaci
            var zywePostacie = wszystkiePostacie.Where(p => p.PunktyZycia > 0).ToList();
            if (zywePostacie.Count <= 1) break;

            var atakujacy = zywePostacie[random.Next(zywePostacie.Count)];

            // Losowanie celu (różnego od atakującego)
            Postac cel;
            do
            {
                cel = zywePostacie[random.Next(zywePostacie.Count)];
            } while (cel == atakujacy);

            // Wykonanie ataku
            atakujacy.Atakuj(cel);

            // Krótka pauza, aby śledzić przebieg walki
            Thread.Sleep(1000);

            if (cel.PunktyZycia > 0)
            {
                cel.Atakuj(atakujacy);
            }

            // Czasem boharter się leczy
            if (atakujacy is Bohater bohater && atakujacy.PunktyZycia < 30 && atakujacy.PunktyZycia > 0)
            {
                Console.WriteLine($"\n{atakujacy.Imie} używa mikstury!");
                bohater.Lecz(30);
            }
            
            Thread.Sleep(2000); // Pauza dla dramatyzmu
        }
        
        // Podsumowanie
        Console.WriteLine("\n=== WALKA ZAKOŃCZONA ===");
        wszystkiePostacie.ForEach(p => p.PokazStatus());

        // Zakończenie gry i ogłoszenie zwycięzcy
        var zwyciezca = wszystkiePostacie.FirstOrDefault(p => p. PunktyZycia > 0);

        if (zwyciezca != null)
            Console.WriteLine($"\n🏆 {zwyciezca.Imie} WYGRYWA!");
        else
            Console.WriteLine("\n 💥 Wszyscy polegli w walce!");
    }
}

Cały system działa jak orkiestra – każdy gra inaczej, ale według tej samej partytury Postac.


7. Podsumowanie: Co zyskujesz dzięki dziedziczeniu i polimorfizmowi?

  • Mniej duplikacji – wspólny kod w klasie bazowej
  • Większa elastyczność – łatwo dodawać nowe postacie
  • Czysty kod – zasady SOLID na pokładzie
  • Łatwe testowanie i utrzymanie
  • Wzorzec projektowy jak z Unity lub Unreal Engine

🧪 Zadanie dla Ciebie!

Stwórz własne klasy:

  • 🛡️ Paladyn z umiejętnością leczenia innych
  • 🧌 Troll z regeneracją HP
  • 🎯 System poziomowania postaci!

Podziel się efektami w komentarzu. Chcę zobaczyć Wasze potwory i epickie starcia!

👉 Jeśli podobał Ci się ten artykuł:

  • Zostaw komentarz!
  • Subskrybuj kanał YT

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *