Статьи по программированию под движком Gold Source
и работе с SDK игры Half-Life
Наследование классов в С++
В этом руководстве я попытаюсь кратко рассказать
про наследование классов в С++ и о том, как
избежать самой главной ошибки новичков в программировании
— копирования одинакового кода целиком.
Итак приступим. Не смотря на то, что наследование
классов — это стандартная функция С++,
постараюсь рассказать о ней максимально
приближенно к программированию в Half-Life. Если вы
захотите узнать о ней вообще — можете почитать
соответствующую литературу по языку :)
Для
начала я хочу разобрать самую распространенную
ошибку новичков в программировании — это переписывание
необходимых функций целиком. Как это выглядит
на практике ?
А вот как: хочет новичок
сделать себе новое оружие — ну допустим там,
автомат какой. И надо ему в этом автомате — лишь
модельку сменить, да и ту, что в руках, а всё
остальное — должно таким же оставаться.
Теперь собственно сам процесс — выделяем текст из mp5.cpp
копируем-вставляем, поменяем ему дефайн на какой-нибудь
WEAPON_AUTOMAT, причем перед этим забыв
его прописать в файле weapons.h. Компилятор ехидно
говорит новичку, что он дурит.
Начинается
усиленный процесс мышления, в ходе которого
рождается первый закон программирования на C++ :
"функции в одном классе не могут иметь
одинаковые имена!" — порой влияние этого закона
становиться настолько сильным, что увидев
перегрузку функций человек в ужасе удаляет этот
код, втайне тихо недоумевая, почему компилятор не
видит столь вопиющего безобразия, но не забывая
ругаться, на его собственные, заботливо
скопированные файлы/строки — делается вывод о
пристрастности компилятора лично к нему, в
голову лезут мысли о том, что гадкий Мелкософт
что-то там опять намудрил, выпустив такой
глючный продукт.
Твердо усвоив
подобный закон, новичок вполне логично
выводит, что раз функции не могут иметь
одинаковые имена — значит им надо присвоить
разные, и чтобы самому не запутаться — каждой
функции в имени добавлет по одной букве —
например P. Вот и получаются у нас загадочные
Mp5p, CgrenadeP и прочие извраты. Любой
мало-мальски опытный кодер, глянув на это
избыточное усложнение программы, непременно подумает пару суровых слов
про автора того кода, а иной ещё
и скопирует эти слова из мозга в голосовой
буфер :) Но как я уже сказал, влиянее
первого закона + страх перед компилятором +
извечное программерское "если это работает, то
лучше я не буду ничего трогать" создают в мозгу
новичка устойчивый психологический барьер,
преодолеть который для многих порой
невыполнимая задача. Вот и программирование для них в
основном сводиться к переименовыванию имен
функций...
Это у нас была теория с
небольшим отклоненением в психологию, а теперь
перейдем непосредственно к практике, и пусть
знания, полученные вами из этой статьи станут
первым шагом на пути к написанию кода с нуля :)
Для начала рассмотрим само понятие
"наследование классов". Я не изучал подробно
какие-либо языки кроме Ассемблера и С++, а поэтому не
знаю есть ли классы в том же Вижуал Бейсике или
Делфи. Предполагаю,
что вы с этим понятием столкнулись впервые и не
знаете, что оно из себя представляет. Итак
наследование классов — это иерархия. Легче всё
представить себе это графически. Откройте файл
cbase.h (лучше всего из Спирита — там нагляднее,
но и обычная Халфа тоже подойдет). Вот собственно
как все выглядит:
прим. G-Cont. иерархия на данный момент дана для Спирита 1.5 CB
Class Hierachy на самом деле — это не класс, просто для удобства.
Давайте подробно рассмортим, какой класс
подчиняется какому, и почему было сделано именно
так, а не иначе.
Как видно из рисунка —
главным классом является CBaseEntity — из него
собственно "произрастают" все остальные. Именно в
классе CBaseEntity находятся всем знакомые
pev->skin, pev->body, pev->sequence и
прочие параметры (если быть точным, то они
входят в глобальную структуру entvars_t, но я не
хочу сильно усложнять материал).
Как вы знаете — все эти pev->skin, pev->body,
pev->sequence доступны из любой энтити и
компилятор вам не скажет, что он видит эту
переменную первый раз в жизни.
Далее на одном и том же уровне идут CPointEntity и
CBaseDelay, чисто от самой pointentity можно
добиться не так уж и много — зачастую, их используют
просто для указания координат, например как
info_player_start.
CBaseDelay — мощная штука
— из него практически вырастает всё, что есть на
сервере. Как видите оттуда идут патроны и подбираемые игроком вещи,
а также его оружие.
Из CBaseToggle у нас получаються основные брашевые энтити (активные блоки) —
всякие двери и поезда, а из CBaseMonster — даже
сам игрок :) Ведь игрок по сути дела — тоже
монстр, только управлемый игроком :).
Данная иерархия очень упрощена и не
содержит ВСЕХ классов сервера, она нужна, просто
для представления что кому принадлежит. Так
допустим, если это CBaseToggle, то в его класс
входят всякие функции для перемещения брашевых
энтитей. А для оружия — всякие там функции
добавления-удаления патронов. Естественно, что
чем глубже класс, тем больше в его распоряжении
всяких функций от надклассов, которые даже не
нужно вызывать — они уже готовы к использованию.
Ну а теперь вернёмся к пресловутому примеру с
оружием.
Вот два рисунка. Задача следующая:
добавить новое оружие, у которого требуется
поменять только лишь модельку и количество патронов.
Собственно это и есть основная идея
наследования классов.
Хотим создать новую
пушку на основе старой, заменив модель ? нет
проблем :)
class Cnewweapon : public CMP5 и
мы имеем в своем распоряжении ВСЕ функции MP5,
причем нам не надо их даже декларировать!
Что у нас изменилось? v_ моделька пушки ? в
каких функциях она у нас прописана?
Замечательно — в Precache и в deploy.
декларируем две эти функции. В "прeкеше" можно
просто прекешить нашу новую модельку и не писать
болше ничего — так или иначе, все остальное уже
прекешировано в Mp5 :) Теперь пишем
коротенькую функцию deploy, сменив там модельку
автомата на свою. Ну и естественно нужно добавить
LINK_ENTITY_TO_CLASS — для нашего нового оружия.
Что ещё? да — информация об оружии:
int CnewWeapon::GetItemInfo(ItemInfo *p)
{
p->pszName = STRING(pev->classname);
p->pszAmmo1 = "9mm"; // пишете хоть ракеты - это влияет только
на тип добавляемых боеприпасов
p->iMaxAmmo1 = _9MM_MAX_CARRY; // сколько
патронов может вместить оружие
p->pszAmmo2
= NULL;
p->iMaxAmmo2 = -1;
p->iMaxClip = GLOCK_MAX_CLIP; // количество патронов в
обойме
p->iSlot = 1;
p->iPosition = 0;
p->iFlags = 0;
p->iId = m_iId = WEAPON_AUTOMAT; // не забудьте продефайнить в
weapons.h ;)
p->iWeight = GLOCK_WEIGHT; // а вес можно вообще не трогать :)
return 1;
}
Естественно все добавляемые функции
нужно внести в класс новой пушки :)
Ну вот мы и получили ружбайку, код которой занимает объём в 6 раз
меньше оригинала (можете даже вставить этот код
в mp5.cpp, если не хотите создавать отдельный
файл), которая обладает всеми свойствами Mp5, и
всё же является совершенно другим,
самостоятельным оружием :)
Я специально
рассмотрел пример с оружием, но вам не составит
никакого труда также добавить нового монстра —
например Отиса, сделав его из Барни :)
Ну вот вроде бы и всё на сегодня — надеюсь я
понятно объяснил материал. И надеюсь, что эти 8
с лишним килобайт текста пойдут вам на пользу.
Пишите комментарии отзывы и вопросы, если
вдруг что непонятно :)
Комментировать статью на форуме half-life.ru
|