Dragokas
Добрая душа
- Сообщения
- 229
- Реакции
- 213
Минимальный пример "Как остановить (удалить) глобальный таймер"
Инфа от Dragokas
Я вижу, как очень часто задают вопросы об ошибках:
Вот минимальные примеры с комментариями, как это грамотно сделать:
1. Единоразовый таймер с хендлом в глобальной переменной (timer_single.sp)
2. Повторяющийся таймер с хендлом в глобальной переменной (timer_repeat.sp)
3. Единоразовый таймер с хендлами для каждого клиента в глобальном массиве (timer_single_per-client.sp)
4. Повторяющийся таймер с хендлами для каждого клиента в глобальном массиве (timer_repeat_per-client.sp)
5. (Без глобальной переменной) Приостанавливаем повторяющийся таймер
- без необходимости управлять хендлом в глобальной переменной (timer_repeat_pause.sp)
-------------------
Общие советы:
Инфа от Dragokas
Я вижу, как очень часто задают вопросы об ошибках:
- Native "KillTimer" reported: Invalid timer handle XXX (error 3)
- Handle XXX is invalid (error 1)
- TIMER_FLAG_NO_MAPCHANGE не останаливает таймер, когда раунд закончен/проигран и т.д.
- когда кто-то желает запустить одноразовый или повторяющийся таймер, а затем остановить его в round_end вручную, или с использованием флага TIMER_FLAG_NO_MAPCHANGE.
Вот минимальные примеры с комментариями, как это грамотно сделать:
1. Единоразовый таймер с хендлом в глобальной переменной (timer_single.sp)
PHP:
#include <sourcemod>
Handle g_iTimer; // глобальная переменная нашего таймера
public void OnPluginStart()
{
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);
HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy);
}
// Давайте создадим таймер в начале раунда:
public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
delete g_iTimer; // удалим предыдущий таймер на случай, если событие "round_start" сработает дважды
g_iTimer = CreateTimer(30.0, Timer_Sample);
// не ставьте здесь TIMER_FLAG_NO_MAPCHANGE !!!
// потому что переменная с хендлом таймера не будет обнулена автоматически в случае, если произойдёт смена карты прежде, чем сработает таймер
}
public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
{
delete g_iTimer; // останавливаем таймер на случай, если он ещё не сработал, таким образом этот старый таймер не будет перенесён на новый раунд и не сработает случайно в процессе загрузки уровня
// ключевое слово "delete" останавливает таймер и автоматически присваивает ноль переменной.
// Заметка: "delete 0" не вызывает срабатывания исключения.
}
public void OnMapEnd() // требуется, поскольку принудительная смена карты не вызывает событие "round_end"
{
delete g_iTimer;
}
public Action Timer_Sample(Handle timer)
{
// делаем что-либо
PrintToChatAll("tick");
// не забываем обнулить здесь переменную-хендл таймера, т.к. после завершения колбека этот хендл станет недействительным
g_iTimer = null;
}
2. Повторяющийся таймер с хендлом в глобальной переменной (timer_repeat.sp)
PHP:
#include <sourcemod>
Handle g_iTimer;
public void OnPluginStart()
{
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);
HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy);
}
public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
// Давайте создадим таймер в начале раунда:
delete g_iTimer; // предотвращает двойной запуск (на всякий случай)
// не ставьте сюда флаг TIMER_FLAG_NO_MAPCHANGE !!!
// потому что переменная с хендлом не будет обнулена
g_iTimer = CreateTimer(1.0, Timer_Sample, _, TIMER_REPEAT);
}
public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
{
delete g_iTimer;
// ключевое слово "delete" останавливает таймер и автоматически присваивает ноль переменной.
// Заметка: "delete 0" не вызывает срабатывания исключения.
}
public void OnMapEnd() // требуется, поскольку принудительная смена карты не вызывает событие "round_end"
{
delete g_iTimer;
}
public Action Timer_Sample(Handle timer)
{
// полезная нагрузка
PrintToChatAll("tick");
return Plugin_Continue;
/*
Замечание 1: если вы желаете принудительно остановить таймер здесь, вам необходимо сначала присвоить ноль переменной с хендлом таймера
g_iTimer = null;
return Plugin_Stop;
Замечание 2: Никогда не пытайтесь применить delete или KillTimer() на хендле таймера внутри своего же колбека (т.е. здесь).
*/
}
3. Единоразовый таймер с хендлами для каждого клиента в глобальном массиве (timer_single_per-client.sp)
PHP:
#include <sourcemod>
Handle g_iTimer[MAXPLAYERS+1];
public void OnPluginStart()
{
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);
HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy);
RegConsoleCmd("sm_timer", CmdTestTimer, "Re-starts timer for testing")
}
Action CmdTestTimer(int client, int args)
{
delete g_iTimer[client];
g_iTimer[client] = CreateTimer(15.0, Timer_Sample, GetClientUserId(client));
}
public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
for ( int i = 1; i <= MaxClients; i++ )
{
if ( IsClientInGame(i) )
{
// запускаем одиночный таймер для каждого клиента отдельно
delete g_iTimer[i];
g_iTimer[i] = CreateTimer(3600.0, Timer_Sample, GetClientUserId(i));
}
}
}
public void OnClientDisconnect(int client)
{
// детальное объяснение см. в следующем примере
delete g_iTimer[client];
}
void Reset()
{
for ( int i = 1; i <= MaxClients; i++ )
{
delete g_iTimer[i];
}
}
public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
{
Reset();
}
public void OnMapEnd()
{
Reset();
}
public Action Timer_Sample(Handle timer, int UserId)
{
int client = GetClientOfUserId(UserId);
if ( client && IsClientInGame(client) )
{
g_iTimer[client] = null;
// что-нибудь делаем
// ...
PrintToChat(client, "tick");
}
}
4. Повторяющийся таймер с хендлами для каждого клиента в глобальном массиве (timer_repeat_per-client.sp)
PHP:
#include <sourcemod>
Handle g_iTimer[MAXPLAYERS+1];
public void OnPluginStart()
{
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);
HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy);
RegConsoleCmd("sm_timer", CmdTestTimer, "Starts timer for testing")
}
Action CmdTestTimer(int client, int args)
{
// останавливаем таймер, если он уже был запущен
// это также установит значение переменной в 0
delete g_iTimer[client];
PrintToChatAll("timer = %i", g_iTimer[client]);
// запускаем повторяющийся таймер
// передаём ссылку на клиента, таким образом мы сможем убедиться, что он не подменён за время, пока таймер на самом деле не сработает
g_iTimer[client] = CreateTimer(15.0, Timer_Sample, GetClientUserId(client), TIMER_REPEAT);
}
public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
for ( int i = 1; i <= MaxClients; i++ )
{
if ( IsClientInGame(i) )
{
// запускаем повторяющийся таймер для каждого клиента
delete g_iTimer[i];
g_iTimer[i] = CreateTimer(15.0, Timer_Sample, GetClientUserId(i), TIMER_REPEAT);
}
}
}
public void OnClientDisconnect(int client)
{
// убедимся, что убили таймер при отключении клиента
delete g_iTimer[client];
}
void Reset()
{
for ( int i = 1; i <= MaxClients; i++ )
{
delete g_iTimer[i]; // останавливаем таймер для каждого игрока
}
}
public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
{
Reset();
}
public void OnMapEnd() // требуется, потому что ForceChangeLevel не вызывает события "round_end"
{
Reset();
}
public Action Timer_Sample(Handle timer, int UserId)
{
// проверяем, является ли тем же клиентом
int client = GetClientOfUserId(UserId);
// проверка, действителен ли
if ( client && IsClientInGame(client) )
{
// что-нибудь делаем
// ...
PrintToChat(client, "tick");
return Plugin_Continue;
/* или, если вам хочется остановить таймер
но, не забудьте сперва обнулить ('null') таймер:
g_iTimer[client] = null;
return Plugin_Stop;
*/
}
// здесь вы не сможете обнулить переменную таймера, потому что вы не знаете правильный индекс клиента для доступа к массиву хендлов, а поскольку клиент уже мог выйти, то GetClientOfUserId не сработает
// поэтому вместо этого мы используем форвард OnClientDisconnect()
return Plugin_Stop;
}
5. (Без глобальной переменной) Приостанавливаем повторяющийся таймер
- без необходимости управлять хендлом в глобальной переменной (timer_repeat_pause.sp)
PHP:
/*
Этот пример демонстрирует 'Шлепок игрока' через 30.0 сек. задержки от начала раунда и повторяет каждые 2 секунды, пока не закончится раунд.
Спасибо Marttt за идею и код.
*/
#pragma semicolon 1
#pragma newdecls required
#include <sourcemod>
#include <sdktools>
#define WAIT_TIME 30.0
float g_fRoundStartTime;
public void OnPluginStart()
{
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);
HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy);
// запускаем таймер, который никогда не будет уничтожен
CreateTimer(2.0, Timer_CheckPlayerZone, _, TIMER_REPEAT);
}
public Action Event_RoundStart(Handle event, const char[] name, bool dontBroadcast)
{
g_fRoundStartTime = GetEngineTime();
}
public Action Event_RoundEnd(Handle event, const char[] name, bool dontBroadcast)
{
g_fRoundStartTime = 0.0;
}
public void OnMapEnd()
{
g_fRoundStartTime = 0.0;
}
public Action Timer_CheckPlayerZone(Handle timer)
{
if ( g_fRoundStartTime == 0.0 ) // "приостанавливаем" таймер между событиями "round_end" - "round_start"
return Plugin_Continue;
if ( GetEngineTime() - g_fRoundStartTime < WAIT_TIME ) // "приостанавливаем" таймер на протяжении 30.0 сек. после "round_start"
return Plugin_Continue;
for ( int i = 1; i <= MaxClients; i++ )
{
if ( IsClientInGame(i) && IsPlayerAlive(i) )
{
SlapPlayer(i, 20, true);
PrintToChat(i, "You are being slapped!");
}
}
return Plugin_Continue;
}
-------------------
Общие советы:
- Не используйте флаг TIMER_FLAG_NO_MAPCHANGE, если вы присваиваете хендл таймера глобальной переменной с целью дальнейшего контроля таймера через эту переменную позднее.
- Флаг TIMER_FLAG_NO_MAPCHANGE может быть безопасно использован, если вы создаёте таймер без назначения его хендла переменной.
- Когда вы убиваете таймер за пределами колбека этого таймера, используйте ключевое слово 'delete'.
- Когда вы убиваете повторяющийся таймер внутри его же колбека, используйте исключительно "return Plugin_Stop"!
- Не используйте там "delete" или"KillTimer()", иначе можете получить ошибки вида:
Plugin "XXX.smx" encountered error 23: Native detected error
[SM] Invalid timer handle XXX (error 3) during timer end, displayed function is timer callback, not the stack trace
- Вы должны всегда присваивать null глобальной переменной таймера в eго колбеке (перед return) в момент перед разрушением хендла, который наступает:
- для повторяющегося таймера, - когда вы передаёте "return Plugin_Stop"
- для единоразового таймера, - автоматически, как только завершается колбек таймера.
Если у вас хендлы таймера хранятся в глобальном массиве и вы не знаете, как найти нужный хендл в массиве из-под колбека таймера, то:- в таймерах вида "для каждого игрока", используйте форвард OnClientDisconnect(), чтобы получить индекс массива.
- в таймерах вида "авто-инкремент":
таких как:
для обнуления глобальной переменной (элемента массива) из-под колбека таймера вы можете воспользоваться вспомогательной функцией:
Использование:
PHP:
const int MAX = 100;
Handle g_iTimer[MAX];
int g_iTimerCount;
public void OnPluginStart()
{
g_iTimer[g_iTimerCount++] = CreateTimer( 1.0, Timer_Sample );
g_iTimer[g_iTimerCount++] = CreateTimer( 2.0, Timer_Sample );
}
для обнуления глобальной переменной (элемента массива) из-под колбека таймера вы можете воспользоваться вспомогательной функцией:
PHP:
void NullifyHandle(Handle timer)
{
for( int i = 0; i <= g_iTimerCount; i++ )
{
if( timer == g_iTimer[i])
{
g_iTimer[i] = null;
break;
}
}
}
Использование:
PHP:
public Action Timer_Sample(Handle timer)
{
// some code
// ...
NullifyHandle(timer); // найдёт соответствие хендла в массиве и обнулит его
return Plugin_Stop;
}
- Удаляйте таймер перед его созданием, чтобы избежать случайного его срабатывания дважды; некоторые события, такие как "round_start" имеют привычку вызываться несколько раз (в определённых играх).
- Чтобы остановить таймер по окончании раунда, используйте оба, и событие "round_end", и форвард "OnMapEnd()", потому что "round_end" не срабатывает, когда карта сменяется принудительно.
Вложения
Последнее редактирование: