Shambler team
Статьи по программированию под движком 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

2005 Shambler Team
  Counter.CO.KZ -     !
Hosted by uCoz