Hallo!
Ich bastel gerade am Netzwerkcode für mein kleines Spiel und nutze dafür
Indy 10. Hab noch nie was mit
Indy gemacht und in den letzten Tagen viel gelesen, viele Demos angeschaut (an dieser Stelle herzlichen Dank an bernhard_LA für seine Demos) und versucht, viel zu verstehen. Und so ganz grob vom Prinzip her funktioniert es schonmal, dass es eine Serveranwendung gibt, wo sich
ein Game-Client problemlos verbinden kann (Betonung liegt auf
ein). Problematisch wird's, wenn ein zweiter Client hinzukommt und damit die Kommunikation etwas komplexer wird.
Mir gehts nicht um das grundlegende theoretische Prinzip einer solchen Kommunikation, sondern um den technischen Aspekt, besonders in Zusammenhang mit
Indy 10.
Zum Beispiel: Jeder Client stellt sich nach der Connection und dem Login beim Server vor, wie sich's für Gentlemen gehört: "Ich bin Charakter Müller, habe das Sprite A, besitze 100 HP und stehe an Position X, Y". Damit weiß der Server bescheid. Er prüft nun in regelmäßigen Abständen die Distanz zwischen den Clients.
Wenn ein Client in Sichtweite des anderen kommt, wird dieser erstmal in eine Liste aufgenommen, wenn er noch nicht drin ist, und dabei wird eine Nachricht an den sichtenden Client vorbereitet "Hey, hier kommt Client X in Sicht" und in eine Nachrichtenschleife gesteckt.
Das Problem, was diesem konkreten Fall deutlich wird: Sobald der 2. Client sich beim Server vorgestellt hat, wird er von der Sichtbarkeits-Routine erfasst, es wird eine Nachricht erstellt und diese wird sofort gesendet.
Der 2. GameClient erhält nun diese "Hey, hier kommt Client X in Sicht" noch bevor er überhaupt mit der Anmeldung richtig durch war, was ihn gegen die Wand fahren lässt, weil er damit nicht rechnet.
Wie oben schon gesagt: Was hier deutlich wird und auch an anderen Stellen auftreten kann, dass sich Nachrichten überlappen können. Dass der Server eine Mitteilung zu machen hat und damit eine laufende Kommunikation zw. Client und Server unterbricht, bzw. stört.
Wie kann ich das verhindern? Der Server darf nichts rausschicken, solange wie eine Clientanfrage noch nicht beantwortet ist.
Mit "Prüfvariablen" kann ich scheinbar wegen der Threadsicherheit nicht arbeiten. Zumindest habe ich das schon versucht und es hat nicht geklappt. Jeder Klient hat eine Variable "IsWaitingForResponse" bekommen. Die wurde am Anfang von "OnExecute" auf TRUE gesetzt und am Ende wieder auf FALSE. In der Nachrichtenschleife wurde diese dann geprüft, mit der Feststellung im Debugger, dass sie irgendwie ständig auf TRUE steht, obwohl keine Kommunikation stattfand ...
Message-Handler
Delphi-Quellcode:
unit UMessageHandler;
interface
uses
Classes, IdTask, IdContext, IdTCPServer, IdSchedulerOfThreadDefault,
IdYarn, IdThread,
UServerTypes;
type
TMessageItem =
record
Id: Integer;
Delete: Boolean;
Context: TIdContext;
Data: TNetworkDataRecord;
end;
PMessageItem = ^TMessageItem;
TMessageHandler =
class(TIdThread)
constructor Create;
destructor Destroy;
override;
protected
FMessageList: TList;
procedure Run;
override;
public
function AddMessage(AMessage: TMessageItem): Integer;
procedure RemoveMessage(Id: Integer);
end;
var
MessageHandler: TMessageHandler;
implementation
uses
SysUtils,
Windows,
Forms,
IdSync,
UEventLogHandler,
UServerClasses,
UServerFunctions;
procedure ClearMessageList(AList: TList);
var
i: Integer;
Item: PMessageItem;
begin
for i := 0
to AList.Count - 1
do
begin
Item := PMessageItem(AList[i]);
Dispose(Item);
end;
AList.Clear;
end;
{ TMessageHandler }
function TMessageHandler.AddMessage(AMessage: TMessageItem): Integer;
var
Item: PMessageItem;
begin
New(Item);
Item^ := AMessage;
Item^.Delete := FALSE;
Item^.Id := FMessageList.Add(Item);
Result := Item^.Id;
end;
constructor TMessageHandler.Create;
begin
inherited Create;
FMessageList := TList.Create;
end;
destructor TMessageHandler.Destroy;
begin
ClearMessageList(FMessageList);
FreeAndNil(FMessageList);
inherited;
end;
procedure TMessageHandler.Run;
var
Buffer: TBytes;
MsgIndex: Integer;
Item: PMessageItem;
Client: TClientContext;
begin
MsgIndex := 0;
while not Terminated
do
begin
if MsgIndex > FMessageList.Count
then
MsgIndex := 0;
if (FMessageList.Count = 0)
then
begin
Sleep(1);
Continue;
end;
Item := PMessageItem(FMessageList[MsgIndex]);
if Item^.Delete
then
begin
RemoveMessage(Item^.Id);
MsgIndex := 0;
Continue;
end;
Client := TClientContext(Item^.Context.Data);
if not Assigned(Client)
then
Continue;
Buffer := DataRecordToByteArray(Item^.Data);
try
if (Item^.Context.Connection.IOHandler.Connected)
and not
Client.WaitingForResponse
then // Client.WaitingForResponse ergab immer TRUE, obwohl es keinen Sinn machte
begin
if not SendBuffer(Item^.Context, Buffer)
then
TIdNotify.NotifyMethod(EventLogHandler.SendBufferError)
else
Item^.Delete := TRUE;
end;
finally
Finalize(Buffer);
end;
//Inc(MsgIndex);
end;
end;
procedure TMessageHandler.RemoveMessage(Id: Integer);
var
i: Integer;
Item: PMessageItem;
begin
for i := 0
to FMessageList.Count - 1
do
begin
Item := PMessageItem(FMessageList[i]);
if Item^.Id = Id
then
begin
FMessageList.Delete(i);
Dispose(Item);
Exit;
end;
end;
end;
end.
OnConnect/OnDisconnect/OnExecute-Behandlung
Delphi-Quellcode:
procedure TMyGameServer.OnClientConnect(AContext: TIdContext);
var
Context: TClientContext;
begin
FStatus := ssClientConnected;
Context := TClientContext.Create(AContext);
Context.ClientsList := FClientsList;
AContext.Data := Context;
AddClient(AContext);
end;
procedure TMyGameServer.OnClientDisconnect(AContext: TIdContext);
begin
FStatus := ssClientDisconnected;
if Assigned(AContext.Data) then
begin
if AContext.Data is TClientContext then
begin
RemoveClient(AContext);
TClientContext(AContext.Data).Free;
end;
AContext.Data := nil;
end;
end;
procedure TMyGameServer.OnServerExecute(AContext: TIdContext);
var
ClientContext: TClientContext;
begin
AContext.Connection.IOHandler.ReadTimeout := 10000;
FStatus := ssProcessingData;
try
ClientContext := TClientContext(AContext.Data);
except
ClientContext := nil;
end;
if Assigned(ClientContext) then
try
if not ClientContext.ProcessData(AContext) then
begin
FStatus := ssError;
TIdNotify.NotifyMethod(EventLogHandler.GetBufferError);
end;
FStatus := ssIdle;
except
FStatus := ssError;
TIdNotify.NotifyMethod(EventLogHandler.ProcessError);
end;
end;
TClientContext.ProcessData
Delphi-Quellcode:
unit UServerClasses;
interface
...
type
TClientContext =
class(TIdThreadSafe)
...
public
function ProcessData(AContext: TIdContext): Boolean;
overload;
end;
implementation
...
function TClientContext.ProcessData(AContext: TIdContext): Boolean;
var
Buffer: TBytes;
begin
FIsWaitingForResponse := FALSE;
// TRUE ergibt, dass der Wert bei Prüfung in Nachrichtenschleife immer auf TRUE steht, obwohl er unten wieder auf FALSE gesetzt wird :(
Result := FALSE;
try
if not ReceiveBuffer(AContext, Buffer)
then
Exit;
if Length(Buffer) = 0
then
Exit;
Lock;
try
try
FData := ByteArrayToDataRecord(Buffer);
finally
Finalize(Buffer);
end;
ProcessData;
finally
Unlock;
end;
Buffer := DataRecordToByteArray(ResponseData);
try
if not SendBuffer(AContext, Buffer)
then
Exit;
finally
Finalize(Buffer);
end;
Result := TRUE;
finally
FIsWaitingForResponse := FALSE;
end;
end;
procedure TClientContext.UpdateClients;
var
i: Integer;
Context: TIdContext;
ClientContext: TClientContext;
DistanceBetween: Double;
begin
for i := 0
to FClientsList.Count - 1
do
begin
if not FCanUpdateClients
then
Exit;
Context := TIdContext(FClientsList[i]);
if Context = FContext
then
Continue;
ClientContext := (Context.Data
as TClientContext);
DistanceBetween := GetDistance(
FCharacterPosition.X, FCharacterPosition.Y,
ClientContext.PositionData.X, ClientContext.PositionData.Y);
// Kommt ein Character in den Sichtbereich?
if DistanceBetween <= CLIENT_VISIBILITY_DISTANCE
then
begin
if not HasNearClient(Context)
and IsValidClient(ClientContext)
then
AddNearClient(Context);
end
else
begin
// Verlässt ein sichtbarer Character den Sichtbereich?
if HasNearClient(Context)
then
begin
if DistanceBetween > CLIENT_VISIBILITY_DISTANCE
then
RemoveNearClient(Context);
end;
end;
end;
end;
procedure TClientContext.AddNearClient(AContext: TIdContext);
begin
FNearClientsList.Add(AContext);
HandleNearClient(clAppear, AContext);
end;
procedure TClientContext.HandleNearClient(ACommand: TCommandsList;
AContext: TIdContext);
var
Data: TNetworkDataRecord;
CharacterData: TIntroductionRecord;
Buffer: TBytes;
i: Integer;
begin
// ...
Broadcast(Data, FNearClientsList);
end;
procedure TClientContext.Broadcast(AData: TNetworkDataRecord; AClientsList: TList);
var
i: Integer;
Msg: TMessageItem;
Context: TIdContext;
List: TList;
begin
List := AClientsList;
for i := 0
to List.Count - 1
do
begin
Context := TIdContext(List[i]);
if Context = FContext
then
Continue;
Msg.Context := Context;
Msg.Data := AData;
MessageHandler.AddMessage(Msg);
end;
end;
end.
Ich hoffe, es ist nicht zu viel und nicht zu wenig Code und dass ihr durchblickt.
Bin für jede Anregung und Hilfe dankbar.