quarta-feira, 25 de novembro de 2009

Criando a Primeira Missão p4 - Scripts (Script dinâmico de detecção - Timers)


Olá Pessoal. Hoje iremos falar sobre os scripts contidos na missão.
Como de praxe, antes de começarmos, para quem entrou a primeira vez no blog. Baixe os arquivos da missão e o entityDatabase.xml no post p1 do "Criando a Primeira Missão", aqui (http://ofp2editor.blogspot.com/2009/11/criando-primeira-missao-metralhadoras.html)
ATENÇÂO: Faça download novamente da missão pois houve correção nos scripts.

Antes de começarmos a falar dos scripts contidos na missão. Vamos falar de como funciona a utilização de scripts no OFP DR.
Os caras da Codemasters foram espertos. Ao invés de criarem toda uma biblioteca de comandos, funções e um interpretador para tudoi isso, como haviam feito os caras da Bohemia Interactive no Antigo Operation Flashpoint ou na série Armed Assault, eles simplesmente pegaram a biblioteca da linguagem Lua e incluíram no Jogo.

O que é a Linguagem LUA ?

Lua é uma linguagem desenvolvida por Brasileiros (êêêê !!!), criada pela Puc-Rio e originalmente projetada para estender aplicações. Lua nasceu e cresceu no Tecgraf, o Grupo de Tecnologia em Computação Gráfica da PUC-Rio. Atualmente, Lua é desenvolvida no laboratório Lablua. Tanto o Tecgraf quanto Lablua são laboratórios do Departamento de Informática da PUC-Rio.
Atualmente é utilizado em outras aplicações como Adobe Photoshop Lightroom, ou mesmo World of Warcraft, justamente para a criação de scripts e prototipagem. Ele também contempla sistemas embarcados através de uma máquina virtual, que também existe dentro do jogo.
A linguagem compreende todos os aspectos de uma linguagem orientada a objetos. Tipos de dados que trabalham como classes, com métodos próprios, semelhantes a javascript ou mesmo java.
Diferentemente do OFP ou Arma, onde você tinha um tratamento muito pobre para tipos de dados, agora com a LUA, o jogo pode contemplar tratamento e formatação de dados numéricos e strings. Métodos complexos de manejo de strings, arrays e outros tipos de dados, como splits, sorts, formats; inclui tipo de dados de Tabela, com definições de campos e registros; contempla funções de I/O permitindo o leitura e gravação de arquivos (durante a missão, o que permite interação com o jogo fora dele, através de um site por exemplo.). Enfim, a linguagem do jogo é tão poderosa, que você poderia até desenvolver uma aplicação para fins comerciais dentro dele (como um conversor de arquivo por exemplo).

Guia de Referências sobre o LUA

Segue abaixo um site com as referências e tutorialis para a Linguagem LUA.


Inserindo Scripts na Missão

Em teoria, não detalhamos os scripts, mas na prática se você acompanhou o blog desde o princípio, você já os criou para trabalhar com eventos como onMissionStart, onEnter ou onDeath.
Para poder trabalhar com scripts LUA dentro da missão iremos utilizar 1 dos 3 componentes na Caixa de Ferramentas Create.




Os componentes que você irá utilizar:
- Level Script caso você precise manipular eventos como onEnter, onDeath, onPlaceableKill, etc.
- Library Script caso você crie uma lib (conjunto) de scripts que serão exportados a outras missões. Ainda não testei como é feita essa exportação de dados.
- Secondary Script para criação de scripts secundários que também não manipula eventos.

Você pode ter mais de 1 tipo de componente, como 2 Levelscript na mesma missão, e caso você repita as funções, por exemplo ambos os componentes scripts chamando funções de onDeath, o jogo irá executar os 2 onDeath, tanto de um componente como do outro.

Em nossa missão existe 2 componentes Level Script. Um componente, o level.lua existe para tratar eventos específicos da missão, como o controle de triggers, as manipulações onDeath, onMission e etc. Um segundo componente, batizado de lib.lua, contém um conjunto de funções para trabalhar com um script responsável por gerenciar a detecção de unidades dentro da missão.

Script de Detecção de Unidades

Nesta missão para dar dinamismo ao jogo, resolvi criar um script que trabalha com uma lógica para detecção um pouco interessante. No jogo, originalmente quando você ataca um soldado, você é identificado de imediato, e o jogo irá chamar eventos onSuspected no mesmo momento, e isso não corresponde muito bem a realidade. A idéia é que nenhuma unidade é detectada e identificada de imediato. Num cenário real, os soldados do esquadrão se deparariam com a ameaça, identificariam a ameaça e depois pediriam reforço, e entre estas ações há um pequeno intervalo de tempo. Mesmo porque apenas um ou dois membro do esquadrão teriam acesso de rádio com a base.
Portanto a regra do script é a seguinte:
A partir do momento que o esquadrão(um echelon) inimigo detecta um player, o player terá x segundos para eliminar todo o esquadrão até que seja informado sua detecção.
Isso inclusive aumenta a coordenação de fogo por parte equipe, já que irá envolver sincronia.

Descrevendo os Scripts lib.lua

Na nossa missão, abra (com um duplo clique) o componente lib.lua.




Você irá cair no editor.



Repare que o componente lib.lua nada mais é do que um arquivo txt com um conjunto de funções "encapsuladas" em um componente separado.
Não existe arquivo externo de scripts. Tudo é feito dentro do próprio editor.
Vamos dissecar as funções deste script:

Antes de tudo iremos definir as variáveis utilizadas por este script, que são:
vetGrp - Matriz que irá guardar os grupos que detectaram presença até agora. É um vetor unidimensional, mas que compreende duas informações a cada "elemento", que são o hora da detecção (vale lembrar que o jogo trata o tempo em milisegundos, onde meia noite irá corresponder ao milisegundo 0) e o nome do elemento que detectou.
INIMIGO_DETECTADO - Variável booleana(verdadeiro / falso) que será responsável por informar se já houve detecção CONFIRMADA de inimigos. É apenas um "flag" caso queiramos parar o script para não ocupar o processador.

As variáveis no LUA respeitam o escopo. Portanto, em lib.lua, essas variáveis são locais dentro do script, portanto serão visívels para todas as funções contidas neste script. Veremos essas variáveis sendo usadas dentro das funções lib.lua.

Existem funções que são ignoradas dentro da missão porque não removi. é Lixo. Entre elas estão as duas primeiras monstra_mensagem (errei e nem percebi), countArray e onMissionStart, que nada mais faz do que tentar criar um componente timer, mas pelo que percebi o onTimer deste componente não funciona se escrito dentro de Level Script com outro nome, que não seja level.lua, por isso tanto o timer como as funções deste timer (libTimerEc e onTimer_libTimerEc) não funcionam.
Função TimerEchelonDeath

Esta função é responsável por incluir um grupo e o tempo máximo para detecção dentro da matriz vetGrp.
Ela recebe os parâmetros grupo e miliSeg, que são o nome do grupo(echelon) que detectou os players (um dado tipo string) e o tempo máximo em milisegundos até que este grupo reporte a detecção para a base.
Logo no início vemos as declarações das variáveis utilizadas dentro da função. Vale lembrar que aqui não precisamos declarar o tipo de dados(string, boolean, inteiro) para a variável, acredito que nem mesmo haveria problema para a execução em não declará-la como local, poderia sim haver conflitos em outros pontos, caso haja nomes semelhantes.

Em seguida vemos as primeiras instruções.

if INIMIGO_DETECTADO then
return;
end;

Aqui estou verificando se a variável local do script lib.lua INIMIGO_DETECTADO já está ativa, e caso esteja, sai da função com o return abortando a inclusão de novos elementos na tabela. Isso porque em nossa missão me basta que inimigos sejam detectados apenas uma vez.

if string.format("%s", grupo) == "nil" then
return;
end
Este trecho nada mais faz do que verificar se a variável grupo é o nome de um grupo válido. Do contrário, caso você crie uma unidade que não seja atrelada a um Echelon, essa variável virá com o conteúdo "nil" (de nulo). Portanto aqui, se o grupo vier como nulo, "nil", a função também é abortada.

Em seguida iremos verificar se o grupo já não está na contido no vetor vetGrp através do trecho:

i=0;
while i < #vetGrp do -- inicio de loop while
igrupo = vetGrp[i + 2];
if string.lower( grupo) == igrupo then -- inicio de if
return;
end -- fim de if
i=i + 2;
end -- fim de loop while

OBS: "--" serve para inserir comentarios
O que acontece é que dentro de um echelon, se você tiver 2 soldados e estes detectarem uma mesma unidade inimiga, serão chamados 2 eventos onSuspected, um para cada soldado que detectou o inimigo. Por isso faço este teste de duplicidade.
O que fiz foi criar uma variávei de indice i e a zerei , em seguida crio um loop while que diz que enquanto a variável i for menor do que o total de elementos do vetor vetGrp ( while i < #vetgrp do) , e o caractere # tem a função de retornar o número total de elementos dentro da matriz.
E dentro deste nó while (que compeende nas linhas de comentario inicio do loop até o fim do loop), irei ler os dados da matriz vetGrp e compará-lo com o parametro grupo passado na função.
Começo retirando o nome do grupo de dentro da matriz atraves da instrução da linha igrupo = vetGrp[i + 2];
Para você acessar o elemento de uma matriz, basta você informar nome_matriz[indice], cujo o indice é um numero de 1 até o total de elementos dessa matriz. Aqui, lembre-se, é uma matriz unidimensional que compreende um par de informações, a hora máxima(em milisegundos) para detecção e o nome do grupo. Portanto i(que se iniciou em 0) com mais 2 teremos 2, e assim no primeiro nó do loop, acessamos o nome do grupo do primeiro par de informações.

Em seguida com o trecho

if string.lower(grupo) == igrupo then
return;
end

Estou comparando se o nome do echelon na variável igrupo que retirei da matriz vetGrp, é igual ao nome do echelon passado como parâmetro através da variável grupo. Caso o grupo informado no parâmetro já exista dentro da matriz, então a função é abortada através do return.
Note que uso a função lower para transformar tudo em minúsculo. Faço isso porque o jogo converte tudo em lowercase. Se você criar um soldado com nome "soldadoDeCampo", caso você dê um display do nome desse soldado, será exibido "soldadodecampo". E atenção com isso, pois o LUA Script é Case Sensitive, ou seja, "soldadodecampo" é diferente de "SoldadoDeCampo" são informações diferentes. Por isso acostumem-se com esta função.
Em seguida incremento a variável de indice em 2 (2 passos), e assim, no próximo nó do loop, caso o índice continue a ser menor do que o numero total de elementos do vetor vetGrp, a variável i estará em 2, e o vetGrp[i+2] será vetGrp[4].
E assim repito as operações até que i seja igual ou maior do que o numero de elementos do vetor vetGrp. E caso não dê duplicidade ele continuará o código pois não abortou a função.

Depois disso, basta apenas inserirmos as informações no vetor vetGrp, através das linhas.

table.insert( vetGrp, (OFP:getMissionTime() + miliSeg));
table.insert( vetGrp, string.lower(grupo) );

A primeira linha iremos fazer a inserção da informação do tempo máximo para o grupo reportar detecção para a base. Como, na verdade, o tempo máximo é a hora do momento + o tempo de espera, fazemos essa soma nessa linha, através da função getMissionTime.
A segunda linha incluimos o nome do grupo, já gravando como minusculo inclusive.
E assim, toda a vez que um grupo AI inimigo detectar um player, ele irá incluir no vetor vetGrp, caso já não esteja incluído, o nome deste grupo, e a hora máxima para que todo este grupo AI inimigo esteja morto, do contrário ele irá reportar detecção.
Função verifDetecta()

O que está função irá fazer é exatamente o que eu disse no parágrafo anterior.
Ela irá percorrer todos os elementos do vetor em pares de 2 em 2, irá comparar se a hora atual é maior ou igual a hora máxima para reportar detecção.
Caso a hora atual seja maior ou igual a hora máxima retirada do vetGrp, então a função conta o número de AIs vivos dentro do echelon inimigo que detectou os players. Caso existam unidades vivas, então é reportado a detecção mudando a variável INIMIGO_DETECTADO para true (verdadeiro).
Vamos depurar a função:
Depois das declarações de variáveis locais da função, temos:
if INIMIGO_DETECTADO then
return;
end;

Aqui faço a mesma verificação da função TimerEchelonDeath, pois caso já tenha sido reportada detecção, então sai da função. Pois não me interessa ficar ocupando processador com tarefas que não serão mais utilizadas.

Neste trecho

i=0;
while i < #vetGrp do
valor = vetGrp[i+1];
grupo = vetGrp[i + 2];
if (OFP:getMissionTime() >= valor) then
if OFP:getEchelonSize(grupo)>0 then
INIMIGO_DETECTADO = true;
return;
else
table.remove(vetGrp, i + 2);
table.remove(vetGrp, i + 1);
end
else
i=i + 2;
end
end

Acima está todo o processo de verificação.
Como na função TimerEchelonDeath, irei percorrer todo o conteúdo do vetor vetGrp utilizando uma variávei de indice, a "i", e o loop "while i < #vetGrp do" , lembrando que o caractere # tem a função de retornar o número de elementos de um vetor.
A cada nó do loop procedo da mesma maneira, só que aqui capturo a informação de hora e grupo. através das linhas:
valor = vetGrp[i+1];
grupo = vetGrp[i + 2];

Depois comparo os valores de hora máxima contida no vetor vetGrp, que foi passada para a variável "valor", com a hora da missão, utilizando a função OFP:getMissionTime().

if (OFP:getMissionTime() >= valor) then
...
else
...
end


Caso a hora da missão seja maior ou igual a hora máxima então irei contar o número de unidades vivas dentro do echelon utilizando:

if OFP:getEchelonSize(grupo)>0 then
...
else
...
end
A função OFP:getEchelonSize retorna apenas as unidades vivas, a função OFP:getEchelonFullSize retorna as vivas + mortas.

E assim, caso tenhamos unidades vivas, irei informar a variável INIMIGO_DETECTADO com o valor true e sair da função, através das linhas:

INIMIGO_DETECTADO = true;
return;
E caso contrário, não tenhamos mais unidades vivas dentro deste echelon (pois todos foram mortos antes do tempo máximo), então não há mais porque verificarmos este echelon, logo, ele pode ser removido do vetor vetGrp para não cupar mais processamento. Isto é feito através das linhas:

table.remove(vetGrp, i + 2);
table.remove(vetGrp, i + 1);

Note que na remoção dos elementos, removo do maior para o menor. Isso porque o vetor funciona como uma pilha de argolas, e o índice aponta exatamente para a posição do elemento da pilha, logo por exemplo, se temos a[1] = "A", a[2]="B" e a[3]="C", quando removermos o a[2], que é "B" o novo, a[2], passará a ser "C".
Portanto, como você pode concluir, as linhas abaixo também funcionariam:

table.remove(vetGrp, i + 1);
table.remove(vetGrp, i + 1);

Lembre-se, que como removemos 2 elementos, não precisamos avançar o índice, na variável "i". E é isso que fazemos quando deixamos esta atribuição (i=i + 2;) no else da comparação entre a hora atual e hora máxima para detecção. E assim, ele só incrementa a variável, caso nenhuma ação tenha sido tomada: nem detecção, e nem morte de todos os elementos do echelon que detectou.
Função retInimigoDetectado()

Essa função nada mais faz do que retornar o valor da variável INIMIGO_DETECTADO para aquem fez a chamada.
Eu poderia ler diretamente o valor da variável INIMIGO_DETECTADO, mas por convenção, desenvolvedores retornam atributos(variáveis que informam uma propriedade) através de métodos(funções).

Função restartTimerEchelonDeath()

Essa função tem a tarefa de reiniciar todo o processo, caso os players já tenham sido detectados e a variável INIMIGO_DETECTADO já esteja com valor true.
A função irá zerar a variável INIMIGO_DETECTADO com o valor false, e zerar o conteúdo da variável vetGrp, da seguinte maneira:

function restartTimerEchelonDeath(grupo, miliSeg)
INIMIGO_DETECTADO = false;
vetGrp={};
end

OBS: caso a linha "vetGrp={};" não esteja em sua missão, inclua. Pois não lembro de ter atualizado antes ou depois de ter upado a missão.


Pronto.
Nossa lib de detecção de players dinâmica está pronta. Iremos, agora, ver como iremos interagir com ela dentro da missão.

Voltando para o script level.lua

Antes de mais nada, para interagir com as funções do level script lib.lua, temos que declará-lo dentro do script level.lua para que suas funções possam ser legíveis.
Dentro do script level.lua, farei isso na função de evento onMissionStart, que lembrando, é chamada automaticamente no início da missão. Abra o script level.lua, clicando 2 vezes sobre ele, procure a função onMissionStart e na última linha da função, você verá:

lib = scripts.mission.lib;

Utilizei o nome da variável lib (que está em negrito) apenas para referenciar facilmente com o verdadeiro nome. Mas o nome desta variável poderia ser o que você preferir. Lembrando a partir desta variável que você terá acesso aos métodos(funções) do script lib.lua.
O nome que você informou em seu level script, no caso lib, deve ser passado na declaração informando o prefixo "scripts.mission.", como fiz.

Após declarado o script dentro do level.lua. Vamos ver agora os principais pontos de interação com o script.
Iremos começar pela função de evento onSuspected. Nela o que faço antes de tudo, é verificar se o que foi detectado é uma unidade west(USMC), se o seu detector é uma unidade east(PLA), e se a unidade west detectada, pertence ao echelon dos jogadores.
Verifico também se o inimigo já não reportou detecção (INIMIGO_DETECTADO) através da função lib.retInimigoDetectado().
Então a unidade que detectou seja válido e ainda não tenha sido reportado a base, iremos incluir este echelon lá na variável vetGrp, informando um tempo randonico de 4000 a 7000 milisegundos (4 a 7 segundos), utilizando a função TimerEchelonDeath(). Veja:

if (OFP:getSide(victim ) == 0) and (OFP:getSide(suspector) == 1) and ( string.lower(OFP:getParentEchelon(victim))== "grpplayer" ) then
if lib.retInimigoDetectado()==false then
lib.TimerEchelonDeath(OFP:getParentEchelon(suspector),
math.random (4000,7000));
end
OFP:setTimer("timerInimigo", 2000);
end

Note que após verificar se o status de detecção continua false e ter informado o grupo echelon e o tempo(sorteando um tempo aleatório entre 4 e 7 segundos), eu ajusto o componente timerInimigo para 2000 milisegundos(2 segundos) usando a função OFP:SetTimer()

Timers

Os timers são componentes utilizados, como vc já presume, para executar uma tarefa após um determinado tempo.
No antigo OFP você poderia gerar um delay no processamento do script simplesmente informando ~N (~ e o número em segundos) o tempo de espera. Isso não existe mais, pelo menos até onde eu sei.
Então, como não posso mais usar um loop while infinito utilizando um delay para uma verificação que se repete, eu tive que criar este componente timer. O timerInimigo é criado dentro da função onMissionStart(), na linha:

OFP:addTimer("timerInimigo", 1000);

O primeiro parâmetro, "timerInimigo" é o nome do componente timer, o segundo parâmetro é o tempo (em milisegundos) para a chamada do evento onTimer.
Quando você cria um componente timer, o jogo irá automaticamente fazer uma chamada a um evento onTimer_, onde aqui, é onTimer_timerInimigo(), veja:

function onTimer_timerInimigo()
lib.verifDetecta();
if lib.retInimigoDetectado()==true then
OFP:setObjectiveState("objSuspeita", "FAILED");
OFP:displaySystemMessage("Detectado");
OFP:disableEvent("onSuspected");
OFP:disableEvent("onTimer_timerInimigo");
end
OFP:setTimer("timerInimigo", 1000);
end

Vale lembrar que o timer não é cíclico. O evento onTimer é chamado uma única vez após o timer tiver sido, ou criado, ou "setado". Por isso, na linha em negrito, "seto" o timer para ser ativado novamente em 1 segundo (1000 milisegundos), através da função OFP:setTimer.

Continuando

E assim depois de ter inserido o grupo e o tempo máximo no vetor vetGrp, lá na função lib.timerEchelonDeath do script lib.lua, é hora de fazer a verificação deste tempo. E é exatamente o que você viu no código da função de evento onTimer_timerInimigo().
A cada passagem pela função de evento onTimer_timerInimigo() faço a verificação com a função lib.verifDetecta (que lembra ? Compara a hora máxima com hora atual, e verifica se o echelon tem unidades vivas), e caso tenha havido report de detecção a variável INIMIGO_DETECTADO lá em lib.lua será mudado para true.
E assim, quando eu fizer a chamada da função lib.retInimigoDetectado() (que nada mais faz do que me retornar o valor da variável INIMIGO_DETECTADO lá da lib.lua), entrarei na condição para executar as linhas:

OFP:setObjectiveState("objSuspeita", "FAILED");
Seta o elemento objSuspeita no mapa para failed.
OFP:displaySystemMessage("Detectado");
Exibe a mensagem "Detectado"

OFP:disableEvent("onSuspected");
Desabilita qualquer chamada ao evento onSuspected, pois o objetivo
já foi pro saco

OFP:disableEvent("onTimer_timerInimigo");
Desabilita o evento onTimer_timerInimigo, já que não precisamos verificar
nada.

Estes eventos é que se sucederão caso haja detecção. Mas você poderia incluir o que quisesse, poderia encerrar a missão como falha, poderia ativar a chamada de um reforço ou apoio, enfim, o que você quiser. Basta incluí-los dentro do nó if then:

if lib.retInimigoDetectado()==true then
...
...
Seus próprios comandos
...
...
end

É óbvio que para quem nunca mexeu com desenvolvimento ou programação, trabalhar com os scripts lua é tarefa um tanto incômoda. Por isso aconselho alguma familiarização dentro do próprio help que passei.
Espero ter sido útil.

Dúvidas, podem postar no comentário que eu recebo por e-mail.

Saudações.

Seguidores