İsimlerine bakarsak, Kahraman üst sınıf, Okçu, Büyücü, vs. de alt sınıf oluyor. Önemli olan hangi işlemlerin ve verinin her Kahramanda mutlaka buldunduğunu belirlemek ve onları Kahraman düzeyinde gerçekleştirmek. Her kahramanın özel işlemleri ve verileri de o özel kahraman düzeyinde gerçekleştirilebilir.
Yukarıdakiler en temel kavramlar. Bunların üzerine yararları görüldükçe design patterns da uygulanabilir.
Benim sıradüzenlerde karşılaştığım bir yapı şöyle:
'
Kahraman
|
KahramanOrtak
/
İksirleÖlen İksirleGüçlenen (bu düzey ve alttaki böyle olmak zorunda değil)
/ / \ /
Okçu ... ... ... Büyücü
'
-
interface Kahraman: Bir interface. Burada her kahramanda olan ve oyunda gereken bütün işlemler bulunur: karşılaş(Kahraman k), yaşıyor_mu(), iç(Eşya e), al(Eşya e), vs.
-
class KahramanOrtak: Her kahramanda bulunan ortak özellikler buradadır. Örneğin her kahramanın canı varsa buraya gelebilir: real can, real seviye, vs.
-
class İksirleÖlen ve İksirleGüçlenen: iç() işlevine İksir geldiğinde can=0 olur veya olmaz.
-
class Okçu, Büyücü, vs. Bunlar her kahramanın en özel işlemlerini ve verilerinin tutarlar.
Yukarıda en alttaki iki düzey aslında öyle olmak zorunda değil. İksirleÖlen veya İksirleGüçlenen gibi ikili (veya çoklu) ayrımlar sıradüzende düzey olamazlar çünkü onun gibi bir çok farklı olanak bulunabilir.
iç(Eşya e) işlevine tekrar bakarsak, belki de bir yöntem, bu türlere belirli eşya içildiğinde ne olacağını anlatan işlevleri kurucularında vermektir:
class Okçu : KahramanOrtak
{
this(/* ... */)
{
// ...
super.içmeEtkisiEkle(new İksirleÖlme(/* ... */));
}
// ...
}
KahramanOrtak'a yapılan o çağrı, KahramanOrtak'ın "içince neler olsun" bilgisini tutan diziye bir bilgi ekler. Eğer iç(Eşya e) işlemi KahramanOrtak düzeyinde gerçekleştirilirse, eldeki "neler olsun" dizisinde ilerlenir ve bu kahramanda gereken değişiklikler yapılabilir.
class KahramanOrtak : Kahraman
{
real can;
İçmeEtkisi[] içinceNelerOlsun;
void içmeEtkisiEkle(İçmeEtkisi ii)
{
içinceNelerOlsun ~= ii;
}
void iç(Eşya e)
{
foreach (etki; içinceNelerOlsun) {
etki.etkile(this, e);
if (!yaşıyor_mu()) {
break;
}
}
}
// ...
}
Garip ayrıntılara daldım ama umarım kahramanlar arasındaki farklılıkların nasıl her tür tarafından bilinmesinin gerekmediğini gösterebilmişimdir. Her ne kadar kahramanın canını KahramanOrtak yönetiyor bile olsa, aslında iksir içince ölünme kavramından onun bile haberi yok. Onun tek bildiği, bir eşya içince bazı etkilerin olabildiği. Tek yaptığı, içilen eşyayı (ve kahramanın kendisini) o etkilere geçirmek ve onların kahramanı etkilemesini sağlamak.
Tabii onun işleyebilmesi için İçmeEtkisi'nin de bir interface olması çak yararlıdır:
interface İçmeEtkisi
{
void etkile(Kahraman k, Eşya e);
}
class İksirleÖlme : İçmeEtkisi
{
void etkile(Kahraman k, Eşya e)
{
// Eğer içilen bir İksir ise k'nin canının sıfır yap
// Hatta, belki de Kahraman'ın asıl türüne göre farklı etki bile yapılabilir.
}
}
NYP'nin sorunlu taraflarından birisi de tam o noktada başlar. Veya şöyle söyleyelim: çoğu NYP dilinde bulunmayan bir olanak tam o noktada gerekir: Yukarıda etkile() içinde İksirleÖlme sınıfında olduğumuzu biliyoruz. Yani eşya bir İksir olduğunda kahramanı öldüreceğiz ama elimizde yalnızca bir Eşya var. Onun İksir olduğunu nasıl bilebiliriz?
Tekrar bakalım: etki.etkile(this, e) yapılan yerde tek bir türe göre havale yapılmıştır. Dil bize tek tür üzerinde havale olanağı sunmuştur: etki'nin asıl türü (çalışma zamanındaki türü, dinamik türü) İksirleÖlme olduğu için programın akışı İksirleÖlme.etki() işlevine geçmiştir. Ama görüldüğü gibi, bu tek tür üzerinden olmaktadır: Program akışı Kahraman'ın ve Eşya'nın çalışma zamanındaki türlerine bağlı olmamıştır. NYP'de bu sorunun adı "double dispatch"tir (hatta bu durumda belki de "multiple dispatch").
Double dispatch C++ veya D tarafından sağlanmaz (başka dillerde var mı, bilmiyorum). Bunun çözümü programcının görevidir. Bir çözüm, Eşya'nın typeid'sine bakmak ve İksir ise ona göre davranmaktır. typeid Eşya'nın çalışma zamanındaki asıl türünü verir. Ama bu bilgiye bakma gereği, saf NYP'den uzaklaşma anlamına gelir ve dilin yetersizliği olarak görülür.
Ama bunlar göz korkutmasın. Ben saf NYP uygulandığında karşılaşılabilecek bir sorun olarak gösterdim. Başka çözümler de bulunabilir. NYP'de (OOP'de) "Double dispatch"i araştırmak gerek.
Temellerimize dönersek, bu gibi işler NYP'nin olmadığı C gibi dillerde bile yapılabildiğine göre aslında o kadar zorlamaya gerek yok. Belki de en basit yöntem neyse onu uygulamak en iyisi... :)
Ali
--
[ Bu gönderi, http://ddili.org/forum'dan dönüştürülmüştür. ]