Muitos
dos conceitos de jogos multiplayer são aplicados também
aos MUDs. Portanto, é extremamente recomendável
a leitura do material sobre programação multiplayer
do club TILT. Iremos
considerar que ele tenha sido lido e devidamente executado.
Diferentemente de
muitos jogos atuais, a conexão de um MUD
é mantida através do protocolo TCP.
Ao invés do UDP,
o TCP mantém uma conexão contínua entre cliente
e servidor, avisando imediatamente a conexão/desconexão
de um usuário. Sua desvantagem em relação
ao UDP é a velocidade: manter a conexão e assegurar
que os dados cheguem ao destino intactos e na devida ordem custa
valiosos micro-segundos, que podem inviabilizar jogos de alta
velocidade. Mas como o ritmo de jogo de um MUD
é bem inferior ao comum nos dias de hoje, a conexão
TCP é a ideal.
Além do TCP,
os MUDs usam também um protocolo de nível
superior, chamado Protocolo de Terminal ou Telnet.
Não vamos, no entanto, nos preocupar com isso, uma vez
que estamos fazendo nosso próprio servidor (e possivelmente,
como bons designers, nosso próprio cliente). Vamos direto
ao que interessa.
Iniciando
Como foi dito, o
MUD é um servidor TCP que envia e recebe
única e exclusivamente texto (strings). Portanto, trabalhar
com ele no Delphi será incrivelmente fácil. Um teste
simples que pode ser feito para experimentar como funcionará
o servidor é o seguinte: Em um novo projeto, crie um TServerSocket
(aba internet) com as propriedades name (server),
port (4000) e active (true).
No evento OnClientConnect
desse componente, escreva:
socket.sendText('Alô
MUnDo'); |
Rode
o programa. Então execute o programa Telnet
(já incluso no windows) na caixa "executar" do
menu iniciar com o comando "telnet 127.0.0.1 4000".
Se tudo der certo o texto "Alô MUnDo" aparecerá
escrito na janela do terminal.
Este
artigo poderia terminar por aqui, já que agora o processo
de criar o MUD se tornou nada mais que um exercício
de programação: receber dados pelo evento OnClientRead
e responder apropriadamente. Porém vamos estudar um pouco
mais a estrutura básica do jogo, criando um sistema que
permita movimentação entre salas, conversa entre
os jogadores conectados e coleta de itens - elementos esseciais
à esse tipo de jogo.
Estruturando
o servidor
O roteiro básico
de uma sessão de jogo é conexão-jogo-desconexão.
A parte "jogo" é a maior e mais complexa, portanto
iremos começar criando nossas rotinas de conexão
e desconexão. Elas devem ser capazes de apresentar informações
básicas aos jogadores e preparar o terreno para o jogo
(incluindo aí criar variáveis para as informações
do usuário).
As informações
do jogador serão guardadas em um record de nome TJogador.
Ele será definido como:
TJogador = record
nome: string; //nome do jogador
senha: string; //senha
posicao: string; //a localização dele no jogo
inventario: string; //os itens que ele têm
socket: TCustomWinSocket; //o objeto responsável pela
//conexão TCP
cmd: string; //o comando sendo digitado
flag: integer; //indica que posição da conexão ele está
end;
|
Entre
as variáveis do jogador, é preciso destacar o Socket,
que guarda as informações da conexão entre
o servidor e o cliente; Cmd que é o comando sendo digitado
pelo usuário (uma vez que os comandos pelo terminal vêm
letra por letra conforme são escritos) e flag, que indica
em que ponto da conexão o jogador está (uma vez
que o MUD tem uma execução linear, precisamos saber
se o jogador está agora mandando o login, a senha ou comandos
de jogo).
Agora
serão definidas algumas constantes que controlarão
a variável flag do jogador:
const
FLDESCONHECIDO = 0; //flag desconhecida
FLLOGIN = 1; //jog mandando nome
FLSENHA = 2; //jog mandando senha
FLNSENHA1 = 3; //novo jog mandando senha
FLNSENHA2 = 4; //novo jog repetindo senha
FLANDANDO = 5; //jogando
|
E
finalmente a variável global que controlará o registro
dos jogadores:
var
jogs: array of TJogador;
|
Esse tipo de variável
(um array dinâmico) pode comportar qualquer número
de elementos (jogadores) à qualquer momento. Ele é
dimensionado usando-se a função "SetLength"
que aloca ou libera memória conforme a necessidade.
No momento em que
uma conexão for criada, essa lista deve ser aumentada,
e uma ligação entre a conexão TCP e o novo
elemento da lista deve ser estabelecida, para que mais tarde seja
possível encontrar o jogador através da conexão.
Portanto, o evento onClientConnect
deve ficar:
procedure TForm1.serverClientConnect(Sender: TObject;
Socket: TCustomWinSocket);
var
jog: integer;
p: Pointer;
begin
jog:= length(jogs);
setLength(jogs, jog + 1); //aumentar lista
jogs[jog].flag:= FLLOGIN; //indicar posição do jogador
socket.SendText(BemVindo.Text); //texto de conexão
getMem(p, 4);
integer(p^):= jog;
socket.Data:= p; //ligar jogador e socket
jogs[jog].socket:= socket;
end;
|
Primeiramente,
a lista de jogadores é aumentada e os dados iniciais (flag)
do jogador são estabelecidos. Em seguida, um texto de boas
vindas é enviado ao jogador ("BemVindo" é
um TMemo que contém
alguma mensagem do tipo "Bem
vindo ao meu MUD! Qual é o seu nome?").
Logo após, é feita uma pequena "mágica"
com ponteiros para ligar a conexão à variável
do jogador recém criada.
O
campo "data" do TCustomWinSocket
é um ponteiro que pode ser usado livremente por qualquer
aplicação. O que faremos é ligar a conexão
(o socket) com a variável do jogador (jogs[jog])
por meio desse campo Data. Para isso separamos na memória
4 bytes (cujo endereço estará nessa variável
data) que guardarão o índice do
jogador na lista (a variável local "jog").
Após alocar a memória e guardar o valor nesse local
o inverso é feito e a variável socket
do jogador passa a guardar o socket da conexão.
Agora
é a vez da desconexão. O evento OnClientDisconnect
fica:
procedure TForm1.serverClientDisconnect(Sender: TObject;
Socket: TCustomWinSocket);
var
jog: integer;
i: integer;
begin
jog:= integer(socket.data^); //achar jogador
if fileExists(jogs[jog].nome) then salvarJog(jog);//salvar
freeMem(socket.data); //liberar memória
//copiar os jogadores da frente da lista para trás
for i:= jog to length(jogs) -2 do begin
jogs[i]:= jogs[i + 1];
integer(jogs[i].socket.Data^):= i;
end;
setLength(jogs, length(jogs) -1); //reduzir lista
end;
|
Primeiro
o índice do jogador é recuperado (da variável
data do socket). Então suas informações
são salvas em um arquivo (para que seu equipamento e sua
posição possam ser recuperados quando reconectar)
e a variável que guardava o índice é liberada.
Em seguida os jogadores que estavam na frente desse jogador na
lista são movidos para trás (ajustando-se seu índice)
e finalmente a lista é diminuída.
O
procedimento SalvarJog
(que deve ser declarado nas definições do formulário)
é o seguinte:
procedure TForm1.SalvarJog(jog: integer);
var
stl: TStringList;
begin
stl:= TStringList.create;
stl.Add(jogs[jog].senha);
stl.add(jogs[jog].posicao);
stl.add(intToStr(jogs[jog].flag));
stl.add(jogs[jog].inventario);
stl.SaveToFile(jogs[jog].nome);
stl.free;
end;
|
Ele
simplesmente cria um TStringList,
adiciona as informações do jogador e salva em um
arquivo com seu nome para futura recuperação.
Com
isso completamos dois dos três passos básicos na
criação de um servidor MUD. Essas
duas áreas do código serão pouco modificadas
no decorrer do desenvolvimento, já que a maior parte da
programação está na terceira etapa, o jogo
em si.
Estruturando o jogo
Estabeleceremos
que cada função do jogo (pegar um objeto, ver o
conteúdo do inventário, etc) ficará em um
procedimento em separado (para facilitar a localização
e compreensão do código). Além disso, cada
uma das diversas etapas do jogo (diferenciados pela variável
flag) também terão seus próprios procedimentos.
Começando pelo evento OnClientRead (que ocorre sempre quando
alguma informação chega da máquina do usuário):
procedure TForm1.serverClientRead(Sender: TObject;
Socket: TCustomWinSocket);
var
cmd: string;
jog: integer;
begin
cmd:= socket.ReceiveText; //informação sendo recebida
jog:= integer(socket.data^); //acha o índice do jogador
if cmd = #13#10 then begin //se for <enter>
//executar função relativa à flag
case jogs[jog].flag of FLLOGIN: cmdLogin(jog);
end;
jogs[jog].cmd:= '';
end else if cmd = #8 then //se for <backspace>
jogs[jog].cmd:= copy(jogs[jog].cmd, 1,
length(jogs[jog].cmd) -1)
else //se for qualquer outro caracter
jogs[jog].cmd:= jogs[jog].cmd + cmd;
end;
|
Esse
evento irá cuidar dos comandos das flags do jogador. Em
primeiro lugar, recebemos o texto e achamos qual jogador está
enviando esse comando. Se o comando enviado for um enter (caracteres
#13#10) então testamos a flag do jogador,
executamos o procedimento indicado e limpamos a variável
de comando. Se a instrução for um backspace (para
limpar o último caracter digitado) copiamos o comando digitado
até agora menos a última letra. Se não for
um desses anteriores, adicionamos o texto recebido ao comando
corrente do jogador.
Por
enquanto, apenas a flag FLLOGIN foi usada, e
portanto o procedimento cmdLogin deve ser criado nas declarações
do formulário ("procedure cmdLogin(jog: integer);")
e implementado como:
procedure TForm1.cmdLogin(jog: integer);
var
stl: TStringList;
begin
jogs[jog].nome:= jogs[jog].cmd; //pegar nome
//se o jogador existe, carregar arquivo e pedir senha
if fileExists(jogs[jog].nome) then begin
stl:= TStringList.create;
stl.LoadFromFile(jogs[jog].cmd);
jogs[jog].senha:= stl[0];
jogs[jog].posicao:= stl[1];
jogs[jog].flag:= FLSENHA;
jogs[jog].inventario:= stl[3];
jogs[jog].socket.SendText(#13#10 +
'Jogador existente. Digite sua senha: ');
stl.free;
end else begin //senão pedir nova senha
jogs[jog].flag:= FLNSENHA1;
jogs[jog].socket.SendText(#13#10 +
'Novo jogador. Digite sua senha: ');
end;
end;
|
O
nome do jogador é transportado da variável Cmd
para a variável Nome. Então, caso
um arquivo com o nome do jogador já exista, ele é
carregado com suas informações e a senha é
pedida ao usuário. Caso esse seja um novo jogador, uma
nova senha é pedida (usamos flags diferentes porque a senha
será pedida duas vezes no caso de um novo jogador).
O
próximo passo são as senhas. Teremos três
procedimentos diferentes para os três tipos de senhas (senha
de um jogador existente, senha de um novo jogador ou repetição
da senha de um novo jogador). Começando pela senha de um
jogador existente:
procedure TForm1.cmdSenha(jog: integer);
begin
if jogs[jog].senha = jogs[jog].cmd then begin
//se a senha está correta
jogs[jog].flag:= FLANDANDO;
jogs[jog].socket.SendText(#13#10 + 'Jogador Conectado!');
end else begin
//se a senha está errada
jogs[jog].flag:= FLLOGIN;
jogs[jog].socket.SendText(#13#10 +
'Senha incorreta! Digite seu nome: ');
end;
end;
|
Se
a senha do jogador (existia e foi carregada no momento do login
do jogador) for igual à senha digitada, conectamos o jogador
e mandamos uma pequena mensagem informando o sucesso da conexão.
Se as senhas forem diferentes, o processo de login é reiniciado
e o nome do usuário é pedido novamente.
Agora
os procedimentos de nova senha:
procedure TForm1.cmdNSenha1(jog: integer);
begin
jogs[jog].senha:= jogs[jog].cmd;
jogs[jog].flag:= FLNSENHA2;
jogs[jog].socket.SendText(#13#10 +
'Digite sua senha novamente: ');
end;
procedure TForm1.cmdNSenha2(jog: integer);
begin
if jogs[jog].senha <> jogs[jog].cmd then begin
//se forem diferentes
jogs[jog].flag:= FLNSENHA1;
jogs[jog].socket.SendText(#13#10 +
'Senhas diferentes. Digite sua senha: ');
end else begin
//se forem iguais
jogs[jog].flag:= FLANDANDO;
jogs[jog].posicao:= '0';
salvarJog(jog);
jogs[jog].socket.SendText(#13#10 + 'Jogador criado!');
end;
end;
|
Na
primeira vez que um novo jogador digita a senha (cmdNSenha1)
ela é guardada na variável respectiva; a flag é
alterada para requisitar a segunda senha e mandamos uma mensagem.
Na
segunda vez que a senha é digitada, ela é comparada
com o valor anterior. Se forem diferentes o processo reinicia
com o pedido da primeira senha. Se forem iguais, o jogador é
conectado, colocado na posição ' 0 '
(mais sobre isso em um momento), suas informações
são salvas e uma mensagem é mandada.
Para
habilitar o uso dos procedimentos descritos, é preciso
modificar o evento OnClientRead
adicionando os três novos métodos das flags:
//executar função relativa à flag
case jogs[jog].flag of
FLLOGIN: cmdLogin(jog);
FLSENHA: cmdSenha(jog);
FLNSENHA1: cmdNSenha1(jog);
FLNSENHA2: cmdNSenha2(jog);
end;
|
E
faça-se o texto
O
servidor criado até agora já apresenta as características
básicas de qualquer jogo multiplayer: Conecta e desconecta
jogadores e guarda em disco os dados necessários para uso
futuro. Agora vêm a parte divertida, a implementação
do jogo.
A
primeira função a ser adicionada será a de
visualizar o local atual do jogador. Portanto, será preciso
guardar as informações dos diversos locais do MUD
(como descrição, objetos no local, monstros, etc)
em arquivos. Como padrão, os arquivos de locais serão
salvos exclusivamente como números, sendo o primeiro local
"0", o segundo "1"
e assim por diante.
As
definições de locais devem conter uma descrição,
informações sobre saídas (qual comando para
sair e para que posição o personagem se desloca
ao executar esse comando) e informações sobre itens
(já que nesse artigo implementaremos até funções
de pegar/soltar objetos).
A
estrutura do arquivo de locais será então:
<descrição>
<descrição>
<...>
<descrição>
* (demarca o fim da seção de descrições)
#,<saída>,<local-destino>
+,<item>
|
A
descrição do local começa na primeira linha
do arquivo e vai até o asterisco. Nesse ponto começam
as definições de saídas (começam pelo
carectere # seguido pela saída e pelo local-destino dessa
saída) e itens (começando pelo caractere + seguido
pelo nome do item).
Serão
precisos alguns locais para testarmos nosso jogo. Junto com o
código desse artigo, encontram-se três locais (arquivos
"0", "1" e "2) porém não
existem limites para a criação de outros.
Como
os comandos do MUD são textuais, e normalmente
na forma (<comando> <parâmetro1> ... <parâmetro
n>), será necessário uma função
para separar o comando e os diversos parâmetros. Essa função
necessitará de um tipo especial chamado TParsedString,
que deve ser definido antes da declaração do forumlário:
type
TParsedString = record //tipo para separação de strings
strings: array of string;
count: integer;
end;
TForm1 = class(TForm)
...
|
E
a função de separação é:
//divide uma string em um array de strings,
//separados pelo "caract"
function ParseString(str, caract: string): TParsedString;
var
p1: integer;
begin
result.count:= 0;
while true do begin
p1:= pos(caract, str);
if (p1 = 0) or (p1 = -1) then break;
setLength(result.strings, result.count + 1);
result.strings[result.count]:= copy(str, 1, p1 -1);
str:= copy(str, p1 + 1, length(str));
result.count:= result.count + 1;
end;
if length(str) > 0 then begin
setlength(result.strings, result.count + 1);
result.strings[result.count]:= str;
result.count:= result.count + 1;
end;
end;
|
O
que esta função faz é procurar um determinado
caracter separador em uma string, e dividi-la sempre que esse
caracter é encontrado. Portanto a string "#,leste,1'
quando dividida pela vírgula é retornada nos índices
[0] = #, [1] = leste e [2] = 1 da variável Strings do tipo
TParsedString.
Essa função será muito útil durante
a criação do jogo.
Com
as funções prontas, é preciso implementar
a função da flag de jogo (FLANDANDO). Inicialmente,
ela será a seguinte:
procedure TForm1.cmdAndando(jog: integer);
var
str: TParsedString;
begin
//se for um comando em branco
if jogs[jog].cmd = '' then exit;
//separar pelos espaços
str:= parseString(jogs[jog].cmd, ' ');
//se for "olhar" para a posição
if jogs[jog].cmd = 'olhar' then mandarPos(jog);
end;
|
O
comando é testado, e caso seja vazio a função
não é executada. Então, ele é separado
pela barra de espaço (embora não usado no momento,
será necessário em breve e portanto já foi
incluído nessa listagem) e finalmente, de acordo com o
comando mandado uma função diferente é executada.
A
primeira função será a de visualizar um local
(MandarPos) que
manda ao jogador sua posição atual. Por partes:
procedure TForm1.mandarPos(jog: integer);
var
stl: TStringList;
lin: integer;
s: string;
str: TParsedString;
begin
stl:= TStringList.create;
stl.loadFromFile(jogs[jog].posicao); //carregar posição
lin:= 0;
jogs[jog].socket.sendText(#13#10); //pular linha
while stl[lin] <> '*' do begin //mandar descrição até o fim
jogs[jog].socket.SendText(stl[lin] + #13#10);
lin:= lin + 1;
end;
|
Definimos
as variáveis que vamos precisar durante a função.
Então um TStringList
é criado e o local do jogador é carregado. Linha
por linha, a descrição do local é mandada
até que seja encontrado o caracter * que indica o fim das
descrições. A segunda parte da função
obtém e manda as saídas:
lin:= lin + 1;
s:= 'Saídas: '; //pegar saídas
if lin < stl.count then
while stl[lin][1] = '#' do begin
str:= parseString(stl[lin], ',');
s:= s + '[' + str.strings[1] + ']';
lin:= lin + 1;
if lin >= stl.count then break;
end;
jogs[jog].socket.sendText(s + #13#10); //mandar saídas
|
Começando
na linha após o asterisco, checamos linha à linha
pelo caracter #. Enquanto ele for encontrado (indicando que a
linha descreve uma saída) o texto é separado e o
nome da saída é concatenado na variável "s"
(que é mandada ao jogador após todas as saídas
terem sido achadas).
A
última parte é a criação da lista
de objetos:
s:= 'Itens: ';
if lin < stl.count then //pegar itens
while stl[lin][1] = '+' do begin
str:= parseString(stl[lin], ',');
s:= s + '[' + str.strings[1] + ']';
lin:= lin + 1;
if lin >= stl.count then break;
end;
jogs[jog].socket.sendText(s + #13#10); //mandar itens
stl.free;
end;
|
Esse
trecho é muito semelhante ao anterior. Enquanto objetos
(caractere +) forem achados, as linhas do texto são divididas
e o objeto é concatenado na lista que é enviada
ao jogador.
Para
finalizar a exibição de locais, o método
cmdAndando precisa ser adicionado ao evento OnClientRead,
que agora permanecerá como:
case jogs[jog].flag of
FLLOGIN: cmdLogin(jog);
FLSENHA: cmdSenha(jog);
FLNSENHA1: cmdNSenha1(jog);
FLNSENHA2: cmdNSenha2(jog);
FLANDANDO: cmdAndando(jog);
end;
|
Além
disso, devem ser adicionadas chamadas ao método MandarPos
assim que o jogador for conectado (nos procedimentos cmdSenha
e cmdNSenha2) para que o usuário saiba
imediatamente onde está sem a necessidade de um comando
logo após conectar-se.
Até
o momento os jogadores têm a capacidade de ver a descrição
do local onde estão, as saídas e os itens presentes
nessa posição. É preciso implementar a movimentação.
A
rotina de movimentação será feita em uma
função chamada TentarSair.
Essa função deve carregar o local do jogador e verificar
se o comando dado é ou não um comando para mover
o personagem para outro local. Ela também retornará
um valor booleano, indicando se o jogador foi ou não movido
para outra posição, para que possamos parar de testar
os comandos assim que descobrirmos qual a intenção
do usuário.
function TForm1.TentarSair(jog: integer): boolean;
var
stl: TStringList;
lin: integer;
str: TParsedString;
begin
result:= false;
stl:= TStringList.create;
stl.loadfromfile(jogs[jog].posicao);
lin:= stl.IndexOf('*') + 1;
while stl[lin][1] = '#' do begin
str:= parseString(stl[lin], ',');
if jogs[jog].cmd = str.strings[1] then begin
jogs[jog].posicao:= str.strings[2];
MandarPos(jog);
result:= true;
break;
end;
lin:= lin + 1;
if (lin > stl.count -1) or (result) then break;
end;
stl.free;
end;
|
Essa
função começa carregando a posição
atual do jogador. Então, começando na linha que
precede o final da descrição, aquelas que descrevem
uma saída são separadas, e caso o comando digitado
pelo jogador seja o mesmo que o comando para saída o jogador
é colocado na nova posição, a descrição
dessa posição é mandada, e o processamento
é interrompido.
Para
que essa função comece seu trabalho, é preciso
chamá-la do método cmdAndando,
que ficará da seguinte maneira:
//se for "olhar" para a posição
if jogs[jog].cmd = 'olhar' then mandarPos(jog)
//testar se é uma saída
else if TentarSair(jog) then exit;
|
Note
que caso o comando "olhar" seja digitado, a rotina é
executada e o método cmdAndando é
terminado. O mesmo ocorre com a função tentarSair:
caso ela retorne verdadeiro (uma saída foi achada e o jogador
foi movido) ela pára a checagem de qualquer outro comando,
agilizando todo o processo.
Alguns
comandos úteis
O
ambiente virtual está pronto, mas ainda há muita
pouca coisa para se fazer nele. Portanto, precisamos de alguns
comandos simples para divertir os futuros jogadores, começando
pelo comando "quem" para que qualquer um possa ver os
jogadores conectados no momento. Essa função será
muito simples:
procedure TForm1.MandarConectados(jog: integer);
var
i: integer;
begin
jogs[jog].socket.SendText('==== Lista de Conectados ==='
+ #13#10);
for i:= 0 to length(jogs) -1 do
jogs[jog].socket.SendText(jogs[i].nome + #13#10);
end;
|
Ela
simplesmente corre a lista de jogadores, mandando para o jogador
que requisitou a lista os nomes de todos os usuários conectados
no momento.
Em
seguida, implementaremos dois procedimentos que serão muito
úteis na expansão do MUD, o método
MandarTodos (que
manda uma linha de texto para todos os usuários conectados)
e AcharJog (que
acha um jogador a partir de seu nome):
procedure TForm1.MandarTodos(s: string);
var
i: integer;
begin
for i:= 0 to length(jogs) -1 do
jogs[i].socket.sendText(s);
end;
function TForm1.AcharJog(nome: string): integer;
var
i: integer;
begin
result:= -1;
for i:= 0 to length(jogs) -1 do
if jogs[i].nome = nome then begin
result:= i;
break;
end;
end;
|
Logo
após esses dois procedimentos, serão criadas duas
funções para chat: Falar e Dizer.
Falar será um comando para mandar uma
linha de chat para todos os usuários conectados, enquanto
Dizer mandará mensagens para um único
jogador (os outros não verão essa conversa):
procedure TForm1.Falar(jog: integer);
var
msg: string;
begin
msg:= copy(jogs[jog].cmd, 7, length(jogs[jog].cmd));
mandarTodos(jogs[jog].nome + ' falou "' + msg +
'"' + #13#10);
end;
procedure TFOrm1.Dizer(jog: integer; str: TParsedString);
var
dest: integer;
s: string;
begin
dest:= acharJog(str.strings[1]);
if dest = -1 then begin
jogs[jog].socket.SendText('Jogador não existe' +
#13#10);
exit;
end;
s:= copy(jogs[jog].cmd, 7 + length(str.strings[1]) + 1,
length(jogs[jog].cmd));
jogs[dest].socket.SendText(jogs[jog].nome + ' disse "' +
s + '"' + #13#10);
end;
|
O
primeiro procedimento é auto-explicativo: o comando do
jogador virá na forma "falar xxxxx" onde xxxx
é a mensagem a ser enviada. Portanto essa parte é
copiada para a variável auxiliar "msg"
e a linha de conversa é mandada para todos os jogadores.
No
segundo procedimento, o primeiro passo é descobrir qual
o jogador-destino da mensagem. Caso não exista um jogador
com esse nome, é enviada uma mensagem de erro ao remetente.
Caso contrário, a mensagem é enviada com sucesso.
O
último trabalho é ajustar o procedimento cmdAndando
para acomodar os novos comandos:
//se for "olhar" para a posição
if jogs[jog].cmd = 'olhar' then mandarPos(jog)
//testar por saídas
else if TentarSair(jog) then exit
//lista de jogadores
else if jogs[jog].cmd = 'quem' then MandarConectados(jog)
//chat para todos os jogadores
else if str.strings[0] = 'falar' then falar(jog)
//chat para um jogador
else if str.strings[0] = 'dizer' then dizer(jog, str);
|
Note
que os comandos "falar" e "dizer"
utilizam não a variável "jogs[jog].cmd"
para checar o comando, mas a variável "str.strings[0]".
Isso porque eles suportam diversos parâmetros, e apenas
a primeira palavra identifica o comando adequado.
Coisas
e mais coisas
Das
metas estabelecidas no início do artigo, só falta
o sistema de objetos e inventário. Serão implementados
três métodos: Pegar (pega um objeto
do local), MostrarInv (mostra a lista de itens
que o jogador possui) e Soltar (solta um item
do inventário para o local).
Começando
pelo procedimento de pegar itens da posição atual:
procedure TForm1.Pegar(jog: integer; str: TParsedString);
var
stl: TStringList;
lin: integer;
item: TParsedString;
begin
stl:= TStringList.create;
stl.loadFromFile(jogs[jog].posicao);
lin:= stl.IndexOf('*');
for lin:= lin to stl.count -1 do
if stl[lin][1] = '+' then begin
item:= parseString(stl[lin], ',');
if pos(str.strings[1], item.strings[1]) > 0 then begin
stl.Delete(lin);
stl.SaveToFile(jogs[jog].posicao);
jogs[jog].inventario:= jogs[jog].inventario +
item.strings[1] + ',';
jogs[jog].socket.SendText('Você pegou ' +
item.strings[1] + #13#10);
break;
end;
end;
stl.free;
end;
|
O
procedimento começa carregando a posição
atual do jogador. A partir do fim das descrições,
as linhas de objeto (indentificados pelo caractere +) são
separadas. Se for detectado que o item que o jogador deseja pegar
está presente nesse item do local, o item é deletado
do local, a descrição é salva e o objeto
é adicionado ao inventário do jogador.
Agora
para a listagem do inventário:
procedure TForm1.MostrarInv(jog: integer);
var
i: integer;
str: TParsedString;
begin
jogs[jog].socket.sendText('=== Inventário ===' + #13#10);
//se o inventário estiver vazio
if jogs[jog].inventario = '' then begin
jogs[jog].socket.sendText('<nada>' + #13#10);
exit;
end;
str:= parseString(jogs[jog].inventario, ',');
for i:= 0 to str.count -1 do //se tiver alguma coisa
jogs[jog].socket.sendText(str.strings[i] + #13#10);
end;
|
O
inventário é salvo de forma <item1>,<item2>,...,<itemn>.
Portanto, caso existam items, a lista é separada e cada
um dos objetos é mandado em uma linha diferente.
E
finalmente, o procedimento Soltar:
procedure TForm1.Soltar(jog: integer);
var
i, j: integer;
inv: TParsedString;
item: string;
stl: TStringList;
begin
inv:= parseString(jogs[jog].inventario, ',');
item:= copy(jogs[jog].cmd, 8, length(jogs[jog].cmd));
for i:= 0 to inv.count -1 do
if pos(item, inv.strings[i]) > 0 then begin
stl:= TStringList.create;
stl.loadFromFile(jogs[jog].posicao);
stl.Add('+,' + inv.strings[i]);
stl.SaveToFile(jogs[jog].posicao);
stl.free;
jogs[jog].inventario:= '';
for j:= 0 to inv.count -1 do
if j <> i then jogs[jog].inventario:=jogs[jog].inventario
+ inv.strings[j] + ',';
jogs[jog].socket.SendText('Você soltou ' + inv.strings[i]
+ #13#10);
break;
end;
end;
|
A
lista de inventário é separada. Em seguida ela é
percorrida até que o item que o jogador deseja soltar seja
encontrado. Quando isso acontece, o arquivo com a descrição
do local é carregado, uma linha é adicionada para
o novo objeto e ele é salvo e fechado. Então, um
novo inventário é montado com os itens restantes
e o processamento é terminado.
Para
habilitar essas três funções, o procedimento
cmdAndando deve ser novamente alterado, dessa
vez de forma definitiva para:
//se for "olhar" para a posição
if jogs[jog].cmd = 'olhar' then mandarPos(jog)
//testar por saídas
else if TentarSair(jog) then exit
//lista de jogadores
else if jogs[jog].cmd = 'quem' then MandarConectados(jog)
//chat para todos os jogadores
else if str.strings[0] = 'falar' then falar(jog)
//chat para um jogador
else if str.strings[0] = 'dizer' then dizer(jog, str)
//pegar um objeto do cenário
else if str.strings[0] = 'pegar' then pegar(jog, str)
//mostrar conteúdo do inventário
else if jogs[jog].cmd = 'inventario' then mostrarInv(jog)
//soltar um objeto do inventário
else if str.strings[0] = 'soltar' then soltar(jog);
|
Pronto!
O sistema de objetos foi criado.
Conclusões
Tendo
completado esse artigo, você terá os elementos básicos
de um servidor MUD, genérico o suficiente
para permitir qualquer ambientação ou tema para
seu jogo.
Jogue
alguns monstros, crie mais locais e você terá um
mundo com uma capacidade incrível de incitar a imaginação
de até milhares de pessoas. Desenvolva seu projeto e teremos
enorme prazer em mostrá-lo e divulgá-lo.
Bom
divertimento.
|