В первом уроке я указал на наличие проблем в работе системы. А именно - неправильно выбирается нация, неправильно отрабатывают бонусы за остров, отключена система исключений. В качестве практического занятия мы попробуем эти проблемы решить.
На всякий случай уточню, что я работаю с ремастером Hokkins'a. Прежде, чем скачивать отсюда файлы, убедитесь что у вас под рукой такая же игра. А лучше - попытайтесь самостоятельно реализовать описанное в уроке. Для этого он и был написан. Практика!
Выбор нации
Для начала, восстановим в памяти, как эта функция выглядит в данный момент:
// выбор нации энкаунтера
int GetRandomNationForMapEncounter(string sIslandID, bool bMerchant)
{
int iNation = -1;
// если передан остров - смотрим его колонию
if(sIslandID != "")
{
int iIsland = FindIsland(sIslandID); // не используется
for(int i = 0; i < MAX_COLONIES; i++)
{
if(colonies[i].island == sIslandID)
{
if (colonies[i].nation != "none")
{
iNation = sti(colonies[i].nation);
break;
}
}
}
}
// дефолтный вес каждой нации
float fEngland = 1.0;
float fFrance = 1.0;
float fSpain = 1.0;
float fHolland = 1.0;
float fPirate = 1.0;
// бонус за ближайшую колонию
if(iNation != -1)
{
switch (iNation )
{
case ENGLAND:
fEngland += 0.2;
break;
case FRANCE:
fFrance += 0.2;
break;
case SPAIN:
fSpain += 0.2;
break;
case HOLLAND:
fHolland += 0.2;
break;
case PIRATE:
fPirate += 0.2;
break;
}
}
// собираем всё в один ряд
float fProbablyNation;
if(bMerchant)
{
fProbablyNation = fEngland + fFrance + fSpain + fHolland;
}
else
{
fProbablyNation = fEngland + fFrance + fSpain + fHolland + fPirate;
}
fProbablyNation = frand(fProbablyNation);
// определяем диапазоны в нашем ряду
// для каждой нации
fFrance = fFrance + fEngland;
fSpain = fFrance + fSpain;
fHolland = fSpain + fHolland;
fPirate = fHolland + fPirate;
// вычисления итоговой нации
// но вместо этого начинаются баги
if(bMerchant == 0)
{
// франция уехала за свой диапазон, своровав его у испании
if(fProbablyNation >= fFrance && fProbablyNation < fSpain)
{
return FRANCE;
}
// и дальше пошло то же самое
if(fProbablyNation >= fSpain && fProbablyNation < fHolland)
{
return SPAIN;
}
if(fProbablyNation >= fHolland && fProbablyNation < fPirate)
{
return HOLLAND;
}
if(fEngland <= fProbablyNation)
{
return ENGLAND;
}
}
else
{
if (rand(2) == 1) return HOLLAND; // можно подумать, что это попытка в реализм
// ведь Голландских торговцев действительно
// было больше всего
// но на самом деле это костыль
if(fProbablyNation >= fFrance && fProbablyNation < fSpain)
{
return FRANCE;
}
if(fProbablyNation >= fSpain && fProbablyNation < fHolland)
{
return SPAIN;
}
if(fProbablyNation >= fHolland && fProbablyNation < fPirate)
{
return HOLLAND;
}
if(fEngland <= fProbablyNation)
{
return ENGLAND;
}
}
// а сами англичане стали пиратами
// или это еще одна, очень тонкая, попытка в реализм?
return PIRATE;
}
Нас интересует фрагмент с вычислением конкретной нации. Чтобы не повторяться, приведу комментарий из первого урока:
Первой в диапазоне стоит Англия. Соответственно, значения от 0.0 до fEngland включительно это Англия. Далее, от fEngland до fFrance - это Франция и так далее.По факту же, Франция засчитывается на диапазоне Испании, Испания на диапазоне Голландии, Голландия на пиратах, а пираты на диапазоне Англии. Да-да, всё что ниже значения fEngland - засчитывается как пираты, потому что этот диапазон не покрыт if-ами. А Англия засчитывается если выпало ровно значение fPirate или диапазон Франции.Так как логику сломали, но явно не понимали почему - появилась дюжина костылей. Первый из них - это переменная bMerchant, которая обозначает, что мы генерируем торговца. Как я уже сказал - Голландия вычисляется на диапазоне пиратов. Но диапазон пиратов не суммируется, если мы вычисляем торговцев. И это приводит нас к тому, что на карте нет Голландских торговцев. Так родилась вот эта строка:if (rand(2) == 1) return HOLLAND;
Тот факт, что нации определяются на диапазонах других наций, ломает еще и логику бонусов за остров. Например, бонус за колонию Испании фактически повышает вероятность спавна Французов и т.д.
То есть нужно поправить диапазоны. Ничего сложного:
if(fProbablyNation <= fEngland) return ENGLAND;
if(fProbablyNation > fEngland && fProbablyNation <= fFrance) return FRANCE;
if(fProbablyNation > fFrance && fProbablyNation <= fSpain) return SPAIN;
if(fProbablyNation > fSpain && fProbablyNation <= fHolland) return HOLLAND;
return PIRATE;
Проверка bMerchant в результирующих вычислениях нам вообще не нужна, так как этот флаг проверяется ещё на этапе создания ряда и пираты туда вообще не включаются, если тип энкаунтера - торговец.
Для починки бонуса за остров никаких дополнительных манипуляций не нужно - ведь он и так работал. Просто этот бонус засчитывался не той нации, что должен.
Перечень доступных наций
Размялись? Перейдём к задаче, где действительно нужно подумать. Ещё одна цитата:
Где-то тут должен быть учёт допустимых для энкаунтера наций, которые описываются в таблице инициализации. Но его нет. Вероятно потому, что в энкаунтере с пиратами из-за этой функции не генерировались пираты. Ведь они вычисляются на диапазоне Аглии, которая исключена из пиратских энкаунтеров.
Речь вот об этом куске кода из таблицы инициализации:
makeref(rEnc, EncountersTypes[ENCOUNTER_TYPE_PIRATE_SMALL]);
rEnc.Type = ENCOUNTER_SCENARIO_WAR;
rEnc.MinRank = 1;
rEnc.MaxRank = 1000;
rEnc.worldMapShip = "sloop";
Enc_AddShips(rEnc, "War", 1, 2);
Enc_ExcludeNation(rEnc, ENGLAND); // Вот эти строки
Enc_ExcludeNation(rEnc, FRANCE); // нас интересуют
Enc_ExcludeNation(rEnc, SPAIN);
Enc_ExcludeNation(rEnc, HOLLAND);
Enc_AddClasses(rEnc, 1, 0, 0, 5, 7);
Enc_AddClasses(rEnc, 1000,0, 0, 5, 7);
Сама функция Enc_ExcludeNation() лежит здесь же, в файле Encounters_init.c:
void Enc_ExcludeNation(ref rEnc, int iNation)
{
string sNation = Nations[iNation].Name;
rEnc.Nation.Exclude.(sNation) = true;
}
Она сохраняет в энкаунтер параметр "Nation.Exclude.(sNation)". Он обозначает нацию, которую не_могут иметь корабли в составе энкаунтера. Простая логика подсказывает, что где-то в функции подбора нации для энкаунтера этот параметр следовало бы проверять.
Сразу хочу отметить, что я не видел, как эта функция устроена в оригинале и в алгоритмах я тоже плохо разбираюсь. Поэтому моё решение далеко не самое лаконичное. Если у вас получилось реализовать это более красиво - обязательно выкладывайте результат в комментариях.
Сначала я опишу своё виденье данной фичи, а затем мы посмотрим код:
Необходимо создать массив из наций, которые могут учавствовать в энкаунтере. При этом нужно сохранить функционал бонуса за остров, поэтому массивов будет два: сначала мы определим веса, а затем перенесём их в новый массив, где будут только нужные нам нации.
В остальном всё остаётся как было - веса собираются в ряд, назначаются диапазоны и определяется победитель. Только теперь это всё будет работать с нашим результирующим массивом допустимых наций, а не со всеми нациями сразу.
// выбор нации энкаунтера
// выбрасываем bMerchant за ненадобностью
// и добавляем в аргументы тип энкаунтера
//int GetRandomNationForMapEncounter(string sIslandID, bool bMerchant)
int GetRandomNationForMapEncounter(string sIslandID, int iEncounterType)
{
// создаём необходимые для работы переменные
float fNation[MAX_NATIONS]; // здесь будут веса
int iAllowedNations[MAX_NATIONS + 1]; // допустимые для энкаунтера нации
float fAllowedNation[MAX_NATIONS + 1]; // диапазоны
int iNationsCount, i; // счётчики
bool bExcluded[MAX_NATIONS]; // флаг что нация исключена
string sNationName; // имя нации
int iNation = -1;
float fProbablyNation;
// если передан остров - смотрим его колонию
if(sIslandID != "") {
//int iIsland = FindIsland(sIslandID); // не используется
for(i = 0; i < MAX_COLONIES; i++) {
if(colonies[i].island == sIslandID) {
if (colonies[i].nation != "none") {
iNation = sti(colonies[i].nation);
break;
}
}
}
}
// дефолтный вес каждой нации
/*
float fEngland = 1.0;
float fFrance = 1.0;
float fSpain = 1.0;
float fHolland = 1.0;
float fPirate = 1.0;
*/
for (i = 0; i < MAX_NATIONS; i++) {
// дефолтный вес каждой нации
fNation[i] = 1.0;
// здесь же проверяем не является ли данная нация исключённой
sNationName = "Nation.Exclude." + Nations[i].Name;
if (CheckAttribute(EncountersTypes[iEncounterType], sNationName)) {
bExcluded[i] = sti(EncountersTypes[iEncounterType].(sNationName));
} else {
bExcluded[i] = false;
}
}
// бонус за ближайшую колонию
/*
if(iNation != -1)
{
switch (iNation )
{
case ENGLAND:
fEngland += 0.2;
break;
case FRANCE:
fFrance += 0.2;
break;
case SPAIN:
fSpain += 0.2;
break;
case HOLLAND:
fHolland += 0.2;
break;
case PIRATE:
fPirate += 0.2;
break;
}
}
*/
if(iNation != -1) fNation[iNation] += 0.2;
// собираем всё в один ряд
/*
if(bMerchant)
{
fProbablyNation = fEngland + fFrance + fSpain + fHolland;
}
else
{
fProbablyNation = fEngland + fFrance + fSpain + fHolland + fPirate;
}
*/
// определяем диапазоны в нашем ряду
// для каждой нации
/*
fFrance = fFrance + fEngland;
fSpain = fFrance + fSpain;
fHolland = fSpain + fHolland;
fPirate = fHolland + fPirate;
*/
fProbablyNation = 0.0;
fAllowedNation[0] = 0.0;
iNationsCount = 0;
for (i = 0; i < MAX_NATIONS; i++) {
if (bExcluded[i] == false) {
iNationsCount++;
// создаём массив допустимых наций
iAllowedNations[iNationsCount] = i;
// и тут же определяем диапазоны в ряду
fAllowedNation[iNationsCount] = fAllowedNation[iNationsCount - 1] + fNation[i];
// собираем ряд
fProbablyNation += fNation[i];
}
}
// вычисляем победителя
/*
if(bMerchant == 0)
{
if(fProbablyNation >= fFrance && fProbablyNation < fSpain)
{
return FRANCE;
}
if(fProbablyNation >= fSpain && fProbablyNation < fHolland)
{
return SPAIN;
}
if(fProbablyNation >= fHolland && fProbablyNation < fPirate)
{
return HOLLAND;
}
if(fEngland <= fProbablyNation)
{
return ENGLAND;
}
}
else
{
if (rand(2) == 1) return HOLLAND;
if(fProbablyNation >= fFrance && fProbablyNation < fSpain)
{
return FRANCE;
}
if(fProbablyNation >= fSpain && fProbablyNation < fHolland)
{
return SPAIN;
}
if(fProbablyNation >= fHolland && fProbablyNation < fPirate)
{
return HOLLAND;
}
if(fEngland <= fProbablyNation)
{
return ENGLAND;
}
}
*/
fProbablyNation = frand(fProbablyNation);
if (fProbablyNation == 0.0) return iAllowedNations[1];
for (i = 1; i <= iNationsCount; i++) {
if (fProbablyNation <= fAllowedNation[i] && fProbablyNation > fAllowedNation[i - 1]) return iAllowedNations[i];
}
return PIRATE;
}
Я оставил блоки изначального кода закомментированными, чтоб было наглядно понятно какой функционал чем был заменён. Давайте разбираться.
Прежде всего - я выбросил аргумент функции bMerchant, т.к. это костыль и он больше не нужен, ведь мы восстанавливаем функционал, который этот костыль заменял.
Вместо него я добавил другой аргумент - iEncounterType, чтоб мы могли получить доступ к списку исключённых наций энкаунтера.
Вместо него я добавил другой аргумент - iEncounterType, чтоб мы могли получить доступ к списку исключённых наций энкаунтера.
ВАЖНО Не забудьте поменять передаваемые аргументы там, где эта функция вызывается.
Далее я создал необходимые для работы переменные - все они прокомментированы.
Теперь, непосредственно, изменения в функционале.
Циклом перебираем все нации. Задаём им значения веса по умолчанию и отмечаем какие из них исключённые.
Добавляем банус за остров, если таковой имеется.
Циклом перебираем все нации. Задаём им значения веса по умолчанию и отмечаем какие из них исключённые.
Добавляем банус за остров, если таковой имеется.
for (i = 0; i < MAX_NATIONS; i++) {
// дефолтный вес каждой нации
fNation[i] = 1.0;
// здесь же проверяем не является ли данная нация исключённой
sNationName = "Nation.Exclude." + Nations[i].Name;
if (CheckAttribute(EncountersTypes[iEncounterType], sNationName)) {
bExcluded[i] = sti(EncountersTypes[iEncounterType].(sNationName));
} else {
bExcluded[i] = false;
}
}
// бонус за ближайшую колонию
if(iNation != -1) fNation[iNation] += 0.2;
Теперь другим циклом переносим допустимые к использованию нации в результирующий массив. В этом же цикле собираем ряд и назначаем диапазоны:
fProbablyNation = 0.0;
fAllowedNation[0] = 0.0;
iNationsCount = 0;
for (i = 0; i < MAX_NATIONS; i++) {
if (bExcluded[i] == false) {
iNationsCount++;
// создаём массив допустимых наций
iAllowedNations[iNationsCount] = i;
// и тут же определяем диапазоны в ряду
fAllowedNation[iNationsCount] = fAllowedNation[iNationsCount - 1] + fNation[i];
// собираем ряд
fProbablyNation += fNation[i];
}
}
И последний цикл определяет победителя:
fProbablyNation = frand(fProbablyNation);
if (fProbablyNation == 0.0) return iAllowedNations[1];
for (i = 1; i <= iNationsCount; i++) {
if (fProbablyNation <= fAllowedNation[i] && fProbablyNation > fAllowedNation[i - 1]) return iAllowedNations[i];
}
Поскольку победитель выбирается циклом, мы не можем охватить if-ами нулевое значение диапазона. Решением этой проблемы служит строка if (fProbablyNation == 0.0) return iAllowedNations[1];.
В целом, логика процессов осталась такой же, как и была. Только теперь это всё работает в циклах, потому что мы не знаем заранее, сколько элементов нужно обрабатывать.
Если у вас что-то не получается - к статье будет приклеплён архив с приведённым кодом. Сможете посмотреть, что вы сделали неправильно.
Примечание:Должен отметить, что исправление бага с выбором наций приводит к снижению количества пиратов на карте. Точнее пиратов осталось столькоже. Но другие энкаунтеры больше не отменяются костылями из-за неверно подобранной нации.
Если игра с таким количеством пиратов кажется вам некомфортной/непривычной - попробуйте немножко увеличить значение rEnc.Chance в таблице инициализации для пиратских энкаунтеров.
Минутка оптимизации
Как я уже говорил - я не силён в алгоритмах. И приведённое выше решение несёт в себе достаточно большое количество вычислений, которые будут вызываться при каждом появлении кораблика на глобалке. Предлагаю, в качестве дополнительных практических упражнений, рассмотреть варианты оптимизации этого кода.
В своём моде Encounters Rebalance я немного разгрузил эту функцию, положив в энкаунтеры список ДОПУСТИМЫХ наций, а не исключённых. Таким образом существенно снизилось количество операций внутри циклов.
Для этого в таблице инициализации появилась функция, которая сохраняет в энкаунтер список всех наций (она вызывается в цикле присвоения энкаунтерам значений по умолчанию), а функция Enc_ExcludeNation() теперь удаляет лишние:
Для этого в таблице инициализации появилась функция, которая сохраняет в энкаунтер список всех наций (она вызывается в цикле присвоения энкаунтерам значений по умолчанию), а функция Enc_ExcludeNation() теперь удаляет лишние:
void Enc_ExcludeNation(ref rEnc, int iNation)
{
string sAttribute = "Nation." + Nations[iNation].Name;
DeleteAttribute(&rEnc, sAttribute);
}
void Enc_AddNations(ref rEnc)
{
int i;
string sNation;
for (i=0; i<MAX_NATIONS; i++) {
sNation = Nations[i].Name;
rEnc.Nation.(sNation) = i;
}
}
Сама функция выбора нации теперь выглядит следующим образом:
int Encounter_GetRandomNation(int iEncounterType, string sIslandID)
{
int iNation = -1;
float fNation[MAX_NATIONS];
float fProbablyNation = 0.0;
int i, iCount;
int iAllowedNations[MAX_NATIONS + 1];
float fAllowedNations[MAX_NATIONS + 1];
aref arAttribute, arNation;
// дефолтные значения веса
for (i = 0; i < MAX_NATIONS; i++) {
fNation[i] = 1.0;
}
// если передан остров - смотрим его колонию
if(sIslandID != "") {
for(i = 0; i < MAX_COLONIES; i++) {
if(colonies[i].island == sIslandID) {
if (colonies[i].nation != "none") {
iNation = sti(colonies[i].nation);
break;
}
}
}
}
// бонус за ближайшую колонию
if(iNation != -1) fNation[iNation] += 0.2;
makearef(arNation, EncountersTypes[iEncounterType].Nation);
iCount = GetAttributesNum(arNation);
// если доступна всего одна нация - сразу возвращаем
if (iCount == 1) {
arAttribute = GetAttributeN(arNation, 0);
return sti(GetAttributeValue(arAttribute));
}
// формируем массив из допустимых для энкаунтера наций
fAllowedNations[0] = 0.0;
for (i = 1; i <= iCount; i++) {
arAttribute = GetAttributeN(arNation, (i - 1));
iNation = sti(GetAttributeValue(arAttribute));
// собираем ряд
fProbablyNation += fNation[iNation];
// создаём массив допустимых наций
iAllowedNations[i] = iNation;
// и тут же определяем диапазоны в ряду
fAllowedNations[i] = fAllowedNations[i - 1] + fNation[iNation];
}
// определяем победителя
fProbablyNation = frand(fProbablyNation);
if (fProbablyNation == 0.0) return iAllowedNations[1];
for (i = 1; i <= iCount; i++) {
if (fProbablyNation <= fAllowedNations[i] && fProbablyNation > fAllowedNations[i - 1]) return iAllowedNations[i];
}
return PIRATE;
}
Циклов осталось столько же, но операций внутри них стало меньше. Массивов тоже стало меньше. Из таких маленьких кубиков оптимизация и собирается.
Надеюсь, натолкнул вас на новые интересные решения.
Жду ваших вариантов реализации в комментариях (:
Жду ваших вариантов реализации в комментариях (: