I wanted to share what I've been playing with this weekend: An extension to GNet to be able to have network-replicated instances of BlitzMax objects with native fields (instead of the TGNetObject slots).
Lots of people on this forums seem interessted in network-programming so I desided to post it here for everyone to share. If you find bugs or make updates I'd be very glad if you could post it back here.
have fun! :)
MaGNet.bmx:
crc32.bmx:
magnetdemo.bmx (rewritten from siblys old gnetdemo):
To make the demo work:
1.) download www.blitzbasic.com/tmp/gnetdemo.zip (marks gnet demo)
2.) unzip it to a folder
3.) put all files from this post into the same folder as #2
4.) Build magnetdemo.bmx
edit: updates to sources and added example.
edit2: updated 20081205 - small error when creating remote object (internal). Added initial remote update that was missing.
Lots of people on this forums seem interessted in network-programming so I desided to post it here for everyone to share. If you find bugs or make updates I'd be very glad if you could post it back here.
have fun! :)
MaGNet.bmx:
' ********************************************************************************************** ' Type TNetGameObject ' ' What: MaGNet ([Ma]han extended [GNet]) is an overlay or extension to GNet aimed to making ' network game development easier and more intuitive for BlitzMax developers. ' ' purpose: It provides means to make networked classes (local/remote) with native fields that are ' replicated automatically through the network. (As opposed to using the "slots" in the ' GNetObject.) ' ' Usage: ' ' (note: It is strongly recommended that you briefly grasp the brl.gnet api first.) ' ' 1) Extend the TNetGameObject class and add your fields: ' ' Type TNetworkPlayer Extends TNetGameObject ' Field name:String ' Field x:Float ' Field y:Float ' Field add_this_meta_tag_to_make_an_non_replicated_field:Int {MAGNET = "0"} ' End Type ' ' (note: only String, Int and Float-fields are replicated) ' ' 2) Instanciate your class and initialize it: ' ' Local client:TGNetHost = CreateGNetHost() ' If Not GNetConnect(client, "127.0.0.1", 12345) Then End ' ' Local player:TNetworkPlayer = New TNetworkPlayer ' player.Init(client) ' ' 3) Create a callback handler with prototype OnRemoteObjectCreated(obj:TNetGameObject) to ' get objects from other peers (players) when they connect: ' ' Global otherPlayers:TList = CreateList() ' ' Function registerOtherPlayer(obj:TNetGameObject) ' otherPlayers.AddFirst(obj) ' End Function ' ' 4) Update the mainloop of your program so it handles updates from the network: ' ' Repeat ' ' '(your game code .... update your own player here) ' ' TNetGameObject.syncLocalObjects() ' GNetSync(client) ' TNetGameObject.syncRemoteObjects(client, registerOtherPlayer) ' ' 'handle remote players ' For Local rPlayer:TNetMessage = EachIn otherPlayers ' If rPlayer.deleted Then ' Print "(" + rPlayer.Ip() + ") Player disconnected" ' End If ' 'example how you can use the remote player: ' GLDrawText rPlayer.name, rPlayer.x, rPlayer.y ' Next ' ' 'flip(); bla..bla..bla (more of your game code) ' Forever ' ' Thats pretty much it. ' ' Additional info: ' To speed things up TNetGameObject objects can also be made to use non-automatic mode. ' This means that the implementator of the TNetGameObject extended class tells whenever ' there have been changes made to the object. This can be done with setters: ' ' ' Type TNetworkPlayer Extends TNetGameObject ' Field name:String ' Field x:Float ' Field y:Float ' Field add_this_meta_tag_to_make_an_non_replicated_field:Int {MAGNET = "0"} ' Method setName(name:String) ' self.name=name ' self.modified = True ' End Method ' Method setX(x:Float) ' self.x=x ' self.modified = True ' End Method ' Method setY(y:Float) ' self.y=y ' self.modified = True ' End Method ' End Type ' ' ' player.Init(client, false) 'we set auto to false ' ' This will prevent MaGNet from manually scanning all local object fields each frame using ' reflection to search for changes. ' ' ********************************************************************************************** 'uncomment (2 lines) when not in BLIde: SuperStrict Import "crc32.bmx" Type TNetGameObject Global _ngoMap:TMap = CreateMap() Global _ngoLocalObjs:TList = CreateList() Const NGO_RESERVED_FIELDS:Int = 1 'Today only slot#0 is used to hold the class-name of the replicated TNetGameObject descendant. Field _gnetObject:TGNetObject Field deleted:Int = False {MAGNET = "0"} Field automatic:Int {MAGNET = "0"} Field modified:Int = False {MAGNET = "0"} Field _typeID:TTypeId Field _host:TGNetHost Field _crcFields:Int[] = New Int[32 - NGO_RESERVED_FIELDS] 'crc for each field Method _internalInit(gnetObject:TGNetObject) _gnetObject = gnetObject _ngoMap.Insert(Self._gnetObject, Self) If GNetObjectLocal(gnetObject) Then SetGNetString(gnetObject, 0, _typeId.Name()) _ngoLocalObjs.AddFirst(Self) EndIf End Method 'Call this, in case you want to delete a local object, to release internal references Method deleteLocal() Assert GNetObjectLocal(_gnetObject), "TNetGameObject.deleteLocal() called on nonlocal object" deleted = True _ngoMap.Remove(_gnetObject) _ngoLocalObjs.Remove(Self) CloseGNetObject(Self._gnetObject) End Method 'Call this after you instanciate a TNetGameObject descendant. 'If auto is true, changes to fields will be scanned automatically, each time syncObjects() is called. ' (this is convinient but slow) 'If auto is false then its up to the implementator or user of a TNetGameObject descendant to flag changes to the object by 'setting modified to true each time a change occurs. ' (this is faster, and can be encapsulated by the implementator of a TNetGameObject descendant by the use of setters. example below.) ' example: ' method setHealth(health:int) ' _health = health ' modified = True ' end method Method Init(host:TGNetHost, auto:Int = True) _host = host automatic = auto _typeId = TTypeId.ForObject(Self) Self._internalInit(CreateGNetObject(host)) End Method Method _updateRemoteObjectFields() Local id:TTypeId = TTypeId.ForObject(Self) 'Write type name first. 'result:+_serializeString(id.Name()) Local gnetFieldIndex:Int = NGO_RESERVED_FIELDS 'index 0 hold the classname (TNetGameObject descendant) For Local fld:TField = EachIn id.EnumFields() Local currTypeId:TTypeId = fld.TypeId() 'If fld.MetaData("MAGNET") = "0" Then If (fld.MetaData("MAGNET") = "0") Or ((currTypeId <> IntTypeId) And (currTypeId <> FloatTypeId) And (currTypeId <> StringTypeId)) Then Continue EndIf 'debug info - uncomment line below 'Print "Ser type:" + fld.TypeId().Name() + ":" + fld.Name() + ":" + fld.TypeId().Name() + ":" + fld.GetString(currNetObj) If currTypeId = IntTypeId Then fld.SetInt(Self, GetGNetInt(Self._gnetObject, gnetFieldIndex)) ElseIf currTypeId = FloatTypeId Then fld.SetFloat(Self, GetGNetFloat(Self._gnetObject, gnetFieldIndex)) ElseIf currTypeId = StringTypeId Then fld.SetString(Self, GetGNetString(Self._gnetObject, gnetFieldIndex)) End If gnetFieldIndex:+1 Assert gnetFieldIndex < 32, "TNetGameObject descendant defines to many fields" If Not (gnetFieldIndex < 32) Then Exit Next End Method 'Call this right after GNetSync() to have automatic updates done. Function syncRemoteObjects(host:TGNetHost, OnRemoteObjectCreated(obj:TNetGameObject) = Null) Local currNetObj:TNetGameObject 'mark deleted Local deletedList:TList = GNetObjects(host, GNET_CLOSED) For Local deletedObject:TGNetObject = EachIn deletedList currNetObj = TNetGameObject(_ngoMap.ValueForKey(deletedObject)) If currNetObj Then currNetObj.deleted = True _ngoMap.Remove(deletedObject) EndIf Next 'add created Local createdList:TList = GNetObjects(host, GNET_CREATED) For Local createdObject:TGNetObject = EachIn createdList Local newTypeId:TTypeId = TTypeId.ForName(createdObject.GetString(0)) 'If untrusted peer talks about illegal types we throw it out: If (Not newTypeId) Or (Not newTypeId.ExtendsType(TTypeId.ForName("TNetGameObject"))) Then ?debug Print "Class does not exist or does not extend TNetGameObject (peer disconnect):'" + createdObject.GetString(0) + "'" ? createdObject._peer.Close() 'this is a hack. there should be a controlled disconnect with removal of all game objects End If Local t:TNetGameObject = TNetGameObject(newTypeId.NewObject()) 'New TNetGameObject subclass t._internalInit(createdObject) t._updateRemoteObjectFields() If OnRemoteObjectCreated Then OnRemoteObjectCreated(t) Next 'update modified Local modifiedList:TList = GNetObjects(host, GNET_MODIFIED) For Local modifiedObject:TGNetObject = EachIn modifiedList currNetObj = TNetGameObject(_ngoMap.ValueForKey(modifiedObject)) If Not currNetObj Then Continue currNetObj._updateRemoteObjectFields() Next End Function 'Call this right before GNetSync() to have automatic updates done. Function syncLocalObjects() For Local lo:TNetGameObject = EachIn _ngoLocalObjs lo._updateLocalObjectFields() Next End Function Global _ngaCrc32:TCrc32 Method _updateLocalObjectFields() If (Not Self.automatic) And (Not modified) Then Return If Self.deleted Then Return If Not _ngaCrc32 Then _ngaCrc32 = New TCrc32 Local id:TTypeId = TTypeId.ForObject(Self) Local gnetFieldIndex:Int = NGO_RESERVED_FIELDS 'index 0 hold the classname (TNetGameObject descendant) Local crcIndex:Int = 0 For Local fld:TField = EachIn id.EnumFields() Local currTypeId:TTypeId = fld.TypeId() If (fld.MetaData("MAGNET") = "0") Or ((currTypeId <> IntTypeId) And (currTypeId <> FloatTypeId) And (currTypeId <> StringTypeId)) Then Continue EndIf 'debug info - uncomment line below 'Print "Ser type:" + fld.TypeId().Name() + ":" + fld.Name() + ":" + fld.TypeId().Name() + ":" + fld.GetString(Self) Local newCrc:Int If currTypeId = IntTypeId Then Local value:Int = fld.GetInt(Self) newCrc = _ngaCrc32.crc_int(value) If Not (_crcFields[crcIndex] = newCrc) Then SetGNetInt(Self._gnetObject, gnetFieldIndex, value) _crcFields[crcIndex] = newCrc EndIf ElseIf currTypeId = FloatTypeId Then Local value:Float = fld.GetFloat(Self) newCrc = _ngaCrc32.crc_float(value) If Not (_crcFields[crcIndex] = newCrc) Then SetGNetFloat(Self._gnetObject, gnetFieldIndex, value) _crcFields[crcIndex] = newCrc EndIf ElseIf currTypeId = StringTypeId Then Local value:String = fld.GetString(Self) newCrc = _ngaCrc32.crc_string("q" + value) 'adding a char so that empty strings get a valid CRC too. If Not (_crcFields[crcIndex] = newCrc) Then SetGNetString(Self._gnetObject, gnetFieldIndex, value) _crcFields[crcIndex] = newCrc EndIf End If crcIndex:+1 gnetFieldIndex:+1 Assert gnetFieldIndex < 32, "TNetGameObject descendant defines to many fields" If Not (gnetFieldIndex < 32) Then Exit Next modified = False End Method 'Unlike the function in enet.bmx this one is usable directly with a enet_peer pointer. '(takes care of the offset into the enet_peer struct correctly) Function _enet_peer_address(peer:Byte Ptr, host_ip:Int Var, host_port:Int Var) Local ip:Int = (Int Ptr peer)[3] Local port:Int = (Short Ptr peer)[8] ?LittleEndian ip = (ip Shr 24) | (ip Shr 8 & $ff00) | (ip Shl 8 & $ff0000) | (ip Shl 24) ? host_ip = ip host_port = port End Function Method Ip:String() Local ip:Int Local port:Int _enet_peer_address(Self._gnetObject._peer._enetPeer, ip, port) Local s:String For Local i:Int = 3 To 0 Step - 1 s = s + String.FromInt((Byte Ptr Varptr ip)[i]) If i > 0 Then s = s + "." Next Return s End Method Method Port:Int() Local ip:Int Local port:Int _enet_peer_address(Self._gnetObject._peer._enetPeer, ip, port) Return port End Method End Type
crc32.bmx:
SuperStrict ' ' MattiasH: converted crc32 code from blitzmax forums to a class. ' Type TCrc32 Global crc_table:Int[] Method New() If Not crc_table Then crc_table = New Int[256] crc_init() End If End Method Method crc_init() Local i:Int Local j:Int Local value:Int For i=0 To 255 value=i For j=0 To 7 If (value & $1) Then value = (value Shr 1) ~ $EDB88320 Else value = (value Shr 1) EndIf Next crc_table[i]=value Next End Method Method crc_bank:Int(bank:TBank) Local bbyte:Int Local crc:Int Local i:Int Local size:Int crc=$FFFFFFFF size=BankSize(bank)-1 For i=0 To size bbyte = PeekByte(bank, i) crc = (crc Shr 8) ~ crc_table[bbyte ~ (crc & $FF)] Next Return ~crc End Method Method crc_stream:Int(stream:TStream) Local bbyte:Int Local crc:Int Local i:Int Local size:Int crc = $FFFFFFFF SeekStream(stream, 0) size = StreamSize(stream) - 1 For i=0 To size bbyte = stream.ReadByte() crc = (crc Shr 8) ~ crc_table[bbyte ~ (crc & $FF)] Next Return ~crc End Method Method crc_int:Int(_int:Int) Local bbyte:Int Local crc:Int Local i:Int crc = $FFFFFFFF For i = 0 To 3 bbyte = (Byte Ptr Varptr _int)[i] crc = (crc Shr 8) ~ crc_table[bbyte ~ (crc & $FF)] Next Return ~crc End Method Method crc_float:Int(_float:Float) Local bbyte:Int Local crc:Int Local i:Int crc = $FFFFFFFF For i = 0 To 3 bbyte = (Byte Ptr Varptr _float)[i] crc = (crc Shr 8) ~ crc_table[bbyte ~ (crc & $FF)] Next Return ~crc End Method Function crc_string:Int(txt:String) Local bbyte:Int Local crc:Int Local i:Int Local size:Int crc=$FFFFFFFF size = Len(txt) For i = 0 To size - 1 bbyte = txt[i] 'Asc(Mid$(txt$,i,1)) crc = (crc Shr 8) ~ crc_table[bbyte ~ (crc & $FF)] Next Return ~crc End Function End Type
magnetdemo.bmx (rewritten from siblys old gnetdemo):
SuperStrict 'uncomment when not in BLIde: Import "MaGNet.bmx" Const GAMEPORT:Int = 12345 Const SLOT_TYPE:Int = 0 'remove this Global GWIDTH:Int = 640 Global GHEIGHT:Int = 480 Global GDEPTH:Int = 0 Global GHERTZ:Int = 30 Graphics GWIDTH,GHEIGHT,GDEPTH,GHERTZ AutoMidHandle True Global playerImage:TImage = LoadImage("ship.png") Global bulletImage:TImage = LoadImage("bullet1.png") Global warpImage:TImage = LoadImage("sparkle.png") Local host:TGNetHost=CreateGNetHost() SeedRnd MilliSecs() Type TDemoNetObjectBase Extends TNetGameObject Field X:Float = 0 Field Y:Float = 0 Field Vx:Float = 0 Field Vy:Float = 0 End Type Type TPlayer Extends TDemoNetObjectBase Field Name:String = "Player" Field Chat:String = "Ready" Field Rot:Float Field Hit:Float Field Score:Int Field BulletCD:Int {MAGNET = "0"} 'bullet cooldown (frames before shoting is allowed again) Field Bullets:TList = CreateList() Method New() X = Rnd(GWIDTH - 64) + 32 Y = Rnd(GHEIGHT - 64) + 32 End Method End Type Type TBullet Extends TDemoNetObjectBase Field lifeTime:Int = 60 End Type 'create local player Global player:TPlayer = New TPlayer 'this is the local player player.Init(host) Global remotePlayers:TList = CreateList() Global remoteBullets:TList = CreateList() Global text_y:Int Function registerIncommingRemoteGameObject(obj:TNetGameObject) If TBullet(obj)'ti.Name() = "TBullet" Then remoteBullets.addFirst(obj) ElseIf TPlayer(obj) 'ti.Name() = "TPlayer" Then remotePlayers.addFirst(obj) Else Local ti:TTypeId = TTypeId.ForObject(obj) Assert False, "Remote peer sent an invalid Object type to us: [" + ti.Name() + "]" EndIf End Function Local chatText:String While Not KeyHit(KEY_ESCAPE) 'FlushMem Local c:Int = GetChar() Select c Case 8 If chatText chatText = chatText[..chatText.length - 1] Case 13 If chatText If chatText[..1] = "/" Local cmd:String = chatText[1..] Local i:Int = cmd.Find(" "), arg:String If i<>-1 arg=cmd[i+1..] cmd=cmd[..i] EndIf Select cmd.ToLower() Case "nick" If arg player.Name = arg 'SetGNetString player,SLOT_NAME,playerName EndIf Case "listen" If Not GNetListen( host,GAMEPORT ) Notify "Listen failed" Case "connect" If Not arg arg="localhost" If Not GNetConnect( host,arg,GAMEPORT ) Notify "Connect failed" End Select Else player.Chat = chatText 'SetGNetString player,SLOT_CHAT,playerChat EndIf chatText = "" EndIf Default If c > 31 And c < 127 chatText:+Chr(c) End Select If KeyDown( KEY_LEFT ) player.Rot:-5 If player.Rot < - 180 player.Rot:+360 'SetGNetFloat player,SLOT_ROT,playerRot Else If KeyDown( KEY_RIGHT ) player.Rot:+5 If player.Rot >= 180 player.Rot:-360 'SetGNetFloat player,SLOT_ROT,playerRot EndIf If KeyDown( KEY_UP ) player.Vx:+Cos(player.Rot) *.15 player.Vy:+Sin(player.Rot) *.15 Else player.Vx:*.99 If Abs(player.Vx) <.1 player.Vx = 0 player.Vy:*.99 If Abs(player.Vy) <.1 player.Vy = 0 EndIf If player.Vx player.X:+player.Vx If player.X < - 8 player.X:+GWIDTH + 16 Else If player.X >= GWIDTH + 8 player.X:-GWIDTH + 16 'SetGNetFloat player,SLOT_X,playerX EndIf If player.Vy player.Y:+player.Vy If player.Y < - 8 player.Y:+GHEIGHT + 16 Else If player.Y >= GHEIGHT + 8 player.Y:-GHEIGHT + 16 'SetGNetFloat player,SLOT_Y,playerY EndIf If player.BulletCD player.BulletCD:-1 If KeyHit(KEY_LALT) And Not player.BulletCD Local newBullet:TBullet = New TBullet newBullet.Init(host) newBullet.X = player.X newBullet.Y = player.Y newBullet.Vx = player.Vx + Cos(player.Rot) * 10 newBullet.Vy = player.Vy + Sin(player.Rot) * 10 'newBullet.lifeTime = 60 'this is the default lifetime player.Bullets.AddFirst(newBullet) player.BulletCD = 5 EndIf 'update local players bullets: For Local bullet:TBullet = EachIn player.Bullets If bullet.lifeTime = 0 bullet.deleteLocal() player.Bullets.Remove(bullet) Continue End If Local dx:Float = bullet.X - GWIDTH / 2 Local dy:Float = bullet.Y - GHEIGHT / 2 Local rot:Float = ATan2(dy, dx) Local accel:Float = 1 / (dx * dx + dy * dy) * 2000 bullet.Vx:-Cos(rot) * accel bullet.Vy:-Sin(rot) * accel bullet.X:+bullet.Vx bullet.Y:+bullet.Vy bullet.lifeTime:-1 Next 'handle remote bullets: For Local rBullet:TBullet = EachIn RemoteBullets If rBullet.deleted Continue Local dx:Float = rBullet.X - player.X Local dy:Float = rBullet.Y - player.Y If dx * dx + dy * dy < 256'144 Local msg:TGNetObject=CreateGNetMessage( host ) If player.Hit SetGNetString MSG, SLOT_TYPE, "gotme" Else SetGNetString MSG, SLOT_TYPE, "hurtme" player.Hit = 1 EndIf SendGNetMessage(MSG, rBullet._gnetObject) EndIf Next If player.Hit player.Hit:-.05 If player.Hit < 0 player.Hit = 0 'SetGNetFloat player, SLOT_HIT, playerHit EndIf TNetGameObject.syncLocalObjects() GNetSync(host) TNetGameObject.syncRemoteObjects(host, registerIncommingRemoteGameObject) 'GNetAccept host For Local MSG:TGNetObject = EachIn GNetMessages(host) Local typ:String = GetGNetString (MSG, SLOT_TYPE) Select typ Case "gotme","hurtme" Local obj:TGNetObject = GNetMessageObject(MSG) Local bullet:TBullet = TBullet(TNetGameObject._ngoMap.ValueForKey(obj)) If bullet And (Not bullet.deleted) 'obj.State()<>GNET_CLOSED If typ = "hurtme" player.Score:+1 'SetGNetInt player,SLOT_SCORE,playerScore EndIf bullet.deleteLocal() player.Bullets.Remove(bullet) 'CloseGNetObject obj EndIf End Select Next Cls text_y = 0 DrawPlayer(player) For Local localBullet:TBullet = EachIn player.Bullets DrawBullet(localBullet) Next For Local remotePlayer:TPlayer = EachIn remotePlayers If remotePlayer.deleted Then remotePlayers.Remove(remotePlayer) Else DrawPlayer(remotePlayer) End If Next For Local remoteBullet:TBullet = EachIn remoteBullets If remoteBullet.deleted Then remoteBullets.Remove(remoteBullet) Else DrawBullet(remoteBullet) End If Next If chatText SetColor 255,255,0 DrawText ">" + chatText, 0, GHEIGHT - 16 SetColor 0,255,0 DrawRect TextWidth(">" + chatText), GHEIGHT - 16, 8, 16 EndIf SetColor 255,255,255 Local txt:String = "MemAllocd:" + GCMemAlloced()' + " BytesIn:" + GNettotalBytesIn() + " BytesOut:" + GNettotalBytesOut() DrawText txt, GWIDTH - TextWidth(txt), 0 SetBlend LIGHTBLEND SetRotation Rnd(360) SetScale Rnd(2,2.125),Rnd(2,2.125) DrawImage warpImage,GWIDTH/2,GHEIGHT/2 SetScale 1,1 SetRotation 0 SetBlend MASKBLEND Flip Wend Function DrawBullet(bullet:TBullet) SetBlend LIGHTBLEND SetColor 255,255,255 DrawImage bulletImage, bullet.x, bullet.y SetBlend MASKBLEND End Function Function DrawPlayer(p:TPlayer) SetRotation p.Rot SetColor 255,255,255 DrawImage playerImage, p.x, p.y If p.hit SetAlpha player.hit SetBlend LIGHTBLEND DrawImage playerImage, p.x, p.y SetBlend MASKBLEND SetAlpha 1 SetColor 255,255,255 EndIf SetRotation 0 DrawText p.name + ":" + p.score, p.x, p.y + 16 If p = player SetColor 255, 255, 255 Else SetColor 0, 128, 255 DrawText p.name + ":" + p.chat, 0, text_y text_y:+16 End Function
To make the demo work:
1.) download www.blitzbasic.com/tmp/gnetdemo.zip (marks gnet demo)
2.) unzip it to a folder
3.) put all files from this post into the same folder as #2
4.) Build magnetdemo.bmx
edit: updates to sources and added example.
edit2: updated 20081205 - small error when creating remote object (internal). Added initial remote update that was missing.