UPDATE(12th June, 2007):
Screenshots of new models/graphics:
http://legion.panda3dprojects.com/
click download and look into the “space” folder
UPDATE(21st May, 2007):
Ok, the old download Link is dead…
The only download link left is:
http://legion.panda3dprojects.com/
Anyway, you won’t find the first (and only fully working) Version there… yet
I’m going to rewrite this first Version so it’ll be easier to download and run it.
I can’t tell when I’ll release any final version (or at least some kind of alpha/beta). I’ve got another project that I’ll have to finish first, before I can even start rewriting the whole concept of my game. (Yes, I’m rewriting it AGAIN, but this time not only rewriting the same code in a different style, but changeing the code fundamentaly)
UPDATE(5th April, 2007):
Thanks to bigfoot I’ve got another web space, that accepts files that are larger than 1MB (The old webspace couldn’t handle files larger than 1MB, so I couldn’t upload a rar or zip file with everything the game needs).
There’re now two versions of the game. The first ist still uploaded as seperate files on the old webspace, the second is uploaded as BadLands.rar on the new web space.
With the second Version I was going to rewrite the whole code, so it’ll become more structured and easier to modify. However, this new code isn’t nearly as functional as the old one. It implements one new feature(Model camera, a frequent request from some friends is implemented on the key ‘c’), but it hasn’t got working network code or a HUD. It is anyway much easier to use, just extract the folder somewhere and start badlands.py, this schould start the ‘game’. Then start the server…
UPDATE:
Download Link:
http://23legion23.funpic.de/downloads/
Some Screenshots of the latest(December 4th 2006) Version:
I’ve finished some working code on my way to develop a First Person Space Shooter (like Wing Commander) for Multiplayer!
The following Code has basic movement, basic collision detection and of course basic networking. So it might be a good start for anyone who wants to try the same.
The code was tested with two players as well as two clients on the same machine as with two clients over the internet. It works. Although I didn’t have the opportunity to test it with more players, but I will soon and update this post, if I had to fix some bugs.
The Key mapping:
After starting the program press either
1 for becoming a Server
2 for becoming a Client and connect to the Server defined in the cfg file
Controls:
w : UP
s : DOWN
a : LEFT
d : RIGHT
q : RollLEFT
e : RollRIGHT
y : move forward
mouse1 : forward
ESC : disconnect (after connect), else quit
So here it is, enjoy and learn!
(I suggest copy&paste before reading, as some comments are long and might not fit into the forums screen width. And DON’T forget to COPY THE CFG FILE)
MSG_NONE = 0
MSG_AUTH = 1
MSG_POS = 2
MSG_QUIT = 3
MSG_CQUIT = 4
MSG_CPOS = 5
timeout=1000 #set timeout for connection attempt
import direct.directbase.DirectStart
from direct.showbase import DirectObject
from pandac.PandaModules import *
from direct.task import Task
import math, sys
from direct.distributed.PyDatagram import PyDatagram
from direct.distributed.PyDatagramIterator import PyDatagramIterator
from direct.gui.DirectGui import *
class main(DirectObject.DirectObject):
def __init__(self):
self.SERVER = 0 #Are we Server? 1 = Server, 0 = Client
self.Clients = {} #PlayerID by Connection
self.PList = {} #Player Stats by PlayerID
self.Models = {} #Models by PlayerID
self.CSP = {} #CollisionSpheres by PlayerID
self.Cnodes = {} #Collision Nodes by PlayerID
file = open("./3dnetwork.cfg", 'r') #read config file
while 1:
str = file.readline()
if (str == ""):
break
part = str.rsplit('=')
if (part[0].strip() == 'myID'):
self.myID = part[1].strip() #set the players id on this machine
if (part[0].strip() == 'server'):
self.serverAddress = part[1].strip() #set the server Address we connect to
if (part[0].strip() == 'port'):
self.port = int(part[1].strip()) #set the port to connect to
file.close()
base.setBackgroundColor(0,0,0)
#set key bindings
self.accept('mouse1', self.forw, [1])
self.accept('mouse1-up', self.forw, [0])
self.accept('escape', self.quit)
self.acceptOnce('w', self.up,[1])
self.acceptOnce('w-up', self.up,[0])
self.acceptOnce('a', self.left,[1])
self.acceptOnce('a-up', self.left,[0])
self.acceptOnce('s', self.down,[1])
self.acceptOnce('s-up', self.down,[0])
self.acceptOnce('d', self.right,[1])
self.acceptOnce('d-up', self.right,[0])
self.acceptOnce('q', self.rleft,[1])
self.acceptOnce('q-up', self.rleft,[0])
self.acceptOnce('e', self.rright,[1])
self.acceptOnce('e-up', self.rright,[0])
self.acceptOnce('y', self.forw,[1])
self.acceptOnce('y-up', self.forw,[0])
#Loading Model as 0 Point for Vector calculation (invisible at world base 0,0,0)
self.basis = loader.loadModel("models/box")
self.basis.reparentTo(render)
self.basis.setPos(0,0,0)
self.basis.hide()
#Load a Model as a reference point in Front of the camera
#needed for vector calculation (also visible as crosshair)
self.null = loader.loadModel('models/box')
self.null.setScale(0.05)
self.null.setColor(255,255,255,1)
self.null.reparentTo(base.camera)
self.null.setPos(0,20,0)
#Load a model to construct the environement from(instancing)
self.beta = loader.loadModel('models/fighter')
self.beta.setScale(0.25)
self.cs = range(2) #setting up the nodePath for the two collision solids
#(for yourself and the static models of the environment
#preparing collision detection
#assign Collsion Solids to NodePath
self.cs[0] = CollisionSphere(0,0,0,15)
self.cs[1] = CollisionSphere(0,0,0,15)
self.traverser=CollisionTraverser() #setting traverser (needed to define fromObjects)
self.pusher=CollisionHandlerPusher() #setting a collision Handler (in this case the pusher,
#which does all the work for us to keep objects from passing through each other
self.betacnode = self.beta.attachNewNode(CollisionNode('cnode1')) #adding a collision node to our Model/Cam
self.camcnode = base.camera.attachNewNode(CollisionNode('cnode2'))
self.betacnode.node().addSolid(self.cs[0]) #adding the collidable geometry to our collision node
self.camcnode.node().addSolid(self.cs[1])
#initialise Collision Detection Handler and Traverser
base.cTrav = self.traverser
self.traverser.traverse(render) #any collsion geometry under render that's not defined as fromObject will be an intoObject
self.pusher.addCollider(self.camcnode, base.camera) #passing collisionNodePath and ModelNodePath to our handler
self.traverser.addCollider(self.camcnode, self.pusher) #making our collisionNode a fromObject with the handler self.pusher
##uncomment to see the collisionSpheres
#self.betacnode.show()
#creating the environment through instancing and positioning the same model
#in circles aroung the world axles
x = 0
for i in range(100):
anglerad=x
self.placeholder = render.attachNewNode("Placeholder")
self.placeholder.setPos(100*math.sin(anglerad), 100.0*math.cos(anglerad),0)
self.placeholder.setColor(255,0,0,1)
self.beta.instanceTo(self.placeholder)
x = x +200
x = 0
for i in range(100):
anglerad=x
self.placeholder2 = render.attachNewNode("Placeholder2")
self.placeholder2.setPos(100*math.sin(anglerad),0, 100.0*math.cos(anglerad))
self.placeholder2.setColor(0,255,0,1)
self.beta.instanceTo(self.placeholder2)
x = x +200
x = 0
for i in range(100):
anglerad=x
self.placeholder3 = render.attachNewNode("Placeholder2")
self.placeholder3.setPos(0,100*math.sin(anglerad), 100.0*math.cos(anglerad))
self.placeholder3.setColor(0,0,255,1)
self.beta.instanceTo(self.placeholder3)
x = x +200
#Disable Mouse control over camera
base.disableMouse()
#INIT NETWORK
self.initNetwork()
##Movement
#Setting up the new HPR relative to the current HPR
#the commented print statements are for debugging
#to achieve a smooth movement and the ability to press multiple keys at once, we have to use a work around:
#if a key is pressed, a task will be started that calculates the new POS/HPR, when the key is released the task is removed again
def rleft(self, keyDown):
if keyDown:
taskMgr.add(self.moveRleft, 'moveRleft')
else:
taskMgr.remove('moveRleft')
self.acceptOnce('q', self.rleft,[1])
self.acceptOnce('q-up', self.rleft,[0])
def moveRleft(self, task):
base.camera.setR(base.camera, -1) #do the actual movement
#updating the PList entry for yourself and then send the data via network
X = base.camera.getX()
Y = base.camera.getY()
Z = base.camera.getZ()
H = base.camera.getH()
P = base.camera.getP()
R = base.camera.getR()
self.PList[self.myID] = [X,Y,Z,H,P,R]
self.updateServer()
return Task.cont
#print '(R)roll r/L: ', base.camera.getR()
def rright(self, keyDown):
if keyDown:
taskMgr.add(self.moveRright, 'moveRright')
else:
taskMgr.remove('moveRright')
self.acceptOnce('e', self.rright,[1])
self.acceptOnce('e-up', self.rright,[0])
def moveRright(self, task):
base.camera.setR(base.camera, +1)
X = base.camera.getX()
Y = base.camera.getY()
Z = base.camera.getZ()
H = base.camera.getH()
P = base.camera.getP()
R = base.camera.getR()
self.PList[self.myID] = [X,Y,Z,H,P,R]
self.updateServer()
return Task.cont
#print '(R)roll R/l: ', base.camera.getR()
def left(self, keyDown):
if keyDown:
taskMgr.add(self.moveLeft, 'moveLeft')
else:
taskMgr.remove('moveLeft')
self.acceptOnce('a', self.left, [1])
self.acceptOnce('a-up', self.left, [0])
def moveLeft(self, task):
base.camera.setH(base.camera, +1)
X = base.camera.getX()
Y = base.camera.getY()
Z = base.camera.getZ()
H = base.camera.getH()
P = base.camera.getP()
R = base.camera.getR()
self.PList[self.myID] = [X,Y,Z,H,P,R]
self.updateServer()
return Task.cont
#print '(H)rot L/r: ', base.camera.getH()
def down(self, keyDown):
if keyDown:
taskMgr.add(self.moveDown, 'moveDown')
else:
taskMgr.remove('moveDown')
self.acceptOnce('s', self.down,[1])
self.acceptOnce('s-up', self.down,[0])
def moveDown(self, task):
base.camera.setP(base.camera, -1)
X = base.camera.getX()
Y = base.camera.getY()
Z = base.camera.getZ()
H = base.camera.getH()
P = base.camera.getP()
R = base.camera.getR()
self.PList[self.myID] = [X,Y,Z,H,P,R]
self.updateServer()
return Task.cont
#print '(P)rot u/D: ', base.camera.getP()
def up(self, keyDown):
if keyDown:
taskMgr.add( self.moveUp, 'moveUp')
else:
taskMgr.remove('moveUp')
self.acceptOnce('w', self.up, [1])
self.acceptOnce('w-up', self.up, [0])
def moveUp(self, task):
base.camera.setP(base.camera, +1)
X = base.camera.getX()
Y = base.camera.getY()
Z = base.camera.getZ()
H = base.camera.getH()
P = base.camera.getP()
R = base.camera.getR()
self.PList[self.myID] = [X,Y,Z,H,P,R]
self.updateServer()
return Task.cont
#print '(P)rot U/d: ', base.camera.getP()
def right(self, keyDown):
if keyDown:
taskMgr.add(self.moveRight, 'moveRight')
else:
taskMgr.remove('moveRight')
self.acceptOnce('d', self.right,[1])
self.acceptOnce('d-up', self.right,[0])
def moveRight(self, task):
base.camera.setH(base.camera, -1 )
X = base.camera.getX()
Y = base.camera.getY()
Z = base.camera.getZ()
H = base.camera.getH()
P = base.camera.getP()
R = base.camera.getR()
self.PList[self.myID] = [X,Y,Z,H,P,R]
self.updateServer()
return Task.cont
#print '(H)rot l/R: ', base.camera.getH()
#calculating forward movement
def forw(self, keyDown):
if keyDown:
taskMgr.add(self.moveForw, 'moveForw')
else:
taskMgr.remove('moveForw')
self.acceptOnce('y', self.forw,[1])
self.acceptOnce('y-up', self.forw,[0])
def moveForw(self, task):
#getting two position vectors from basis to camera and from basis to
#the point in front of the camera
x = base.camera.getPos(self.basis)
y = self.null.getPos(self.basis)
#calculating the pointing vector
g = x-y
#normalize
g.normalize()
#aplly translation
base.camera.setFluidPos(base.camera.getPos()-g)
X = base.camera.getX()
Y = base.camera.getY()
Z = base.camera.getZ()
H = base.camera.getH()
P = base.camera.getP()
R = base.camera.getR()
self.PList[self.myID] = [X,Y,Z,H,P,R]
self.updateServer()
return Task.cont
## Networking
def initNetwork(self): #common definitions for Client and Server
print "Network INIT"
self.cManager = QueuedConnectionManager() #the names (like self.cManager) are arbitrary
self.cListener = QueuedConnectionListener(self.cManager, 0)
self.cReader = QueuedConnectionReader(self.cManager, 0)
self.cWriter = ConnectionWriter(self.cManager,0)
self.acceptOnce('1', self.startServer) #Press 1 to become Server
self.acceptOnce('2', self.startClient) #Press 2 to connect as Client to a Server
def startServer(self):
#set up socket on port (defined in the 3dnetwork.cfg)
self.tcpSocket = self.cManager.openTCPServerRendezvous(self.port, timeout)
# Tell the listener to listen for new connections on this socket
self.cListener.addConnection(self.tcpSocket)
# Start Listener task
taskMgr.add(self.listenTask, "serverListenTask",-40)
# Start Read task
taskMgr.add(self.readTask, "serverReadTask", -39)
self.SERVER = 1 #Now we are Server
self.myID="123" #setting Server ID to 123 by default
self.PList[self.myID] = [0,0,0,0,0,0] #adding yourself to the Player List
#starting a task to update the world, as network transfers only update the lists this task will actually update the "visible" stuff
taskMgr.add(self.updateWorld, "updateWorldTask")
print "Server STARTED"
def startClient(self):
#connect to server on port and try until timeout is reached
self.Connection = self.cManager.openTCPClientConnection(self.serverAddress, self.port, timeout)
self.cReader.addConnection(self.Connection)
taskMgr.add(self.readTask, "serverReaderPollTask", -39)
taskMgr.add(self.updateWorld, "updateWorldTask")
self.SERVER = 0 #now we are Client
self.PList[self.myID] = [40,40,40,0,0,0] #Add yourself to the Player List and set a position away from the servers start Pos
base.camera.setX(40)
base.camera.setY(40)
base.camera.setZ(40)
print "Client STARTED"
self.csendMsgAuth()
def csendMsgAuth(self): #send the first datagram to the server
#construct the AUTH package
dta = PyDatagram()
dta.addUint16(MSG_AUTH) #Set Package Type
dta.addString(self.myID)
stats = self.PList[self.myID]
num = len(stats)
for i in range(num):
dta.addFloat64(stats[i])
self.cWriter.send(dta, self.Connection) #Send Data Package to Server
def listenTask(self, task): #the task that listens for new connections
"""
Accept new incoming connections from the client
"""
# Run this task after the dataLoop
# If there's a new connection Handle it
if self.cListener.newConnectionAvailable():
rendezvous = PointerToConnection()
netAddress = NetAddress()
newConnection = PointerToConnection()
if self.cListener.getNewConnection(rendezvous,netAddress,newConnection):
newConnection = newConnection.p()
# tell the Reader that there's a new connection to read from
self.cReader.addConnection(newConnection)
#Clients[newConnection] = netAddress.getIpString()
self.lastConnection = newConnection
print "Got a connection!"
else:
print "getNewConnection returned false"
return Task.cont
def readTask(self, task): #this tasks waits for datagrams, if one arrives with valig MSG ID, datagram handler will be called
while 1:
(datagram, pkg, msgID) = self.nonBlockingRead(self.cReader)
if msgID is MSG_NONE:
break
else:
self.datagramHandler(pkg, msgID)
return Task.cont
def nonBlockingRead(self,qcr): #the actual reading task
"""
Return a datagram iterator and type if data is available on the
queued connection reader
"""
if self.cReader.dataAvailable():
datagram = NetDatagram()
if self.cReader.getData(datagram):
pkg = PyDatagramIterator(datagram)
msgID = pkg.getUint16()
else:
pkg = None
msgID = MSG_NONE
else:
datagram = None
pkg = None
msgID = MSG_NONE
# Note, return datagram to keep a handle on the data
return (datagram, pkg, msgID)
def datagramHandler(self, pkg, msgID): #Common Handler
#Every MSG ID has its own datagram format, so each one needs its own handler
if (msgID == MSG_AUTH):
#Data from a Datagram is read in the order and the format it was put there from the sender
PlayerID=pkg.getString()
self.Clients[self.lastConnection] = PlayerID
X=pkg.getFloat64()
Y=pkg.getFloat64()
Z=pkg.getFloat64()
H=pkg.getFloat64()
P=pkg.getFloat64()
R=pkg.getFloat64()
StatList = [X,Y,Z,H,P,R]
self.PList[PlayerID] = StatList
self.updateClients()
elif (msgID == MSG_POS):
NUM = pkg.getUint16()
for i in range(NUM):
PlayerID=pkg.getString()
if (PlayerID != self.myID):
X=pkg.getFloat64()
Y=pkg.getFloat64()
Z=pkg.getFloat64()
H=pkg.getFloat64()
P=pkg.getFloat64()
R=pkg.getFloat64()
StatList = [X,Y,Z,H,P,R]
self.PList[PlayerID] = StatList
if (self.SERVER == 1):
self.updateClients()
elif (msgID == MSG_QUIT):
#This handler handels if someone quits the game
if (self.SERVER == 0):
print "server quit"
self.cManager.closeConnection(self.Connection)
self.accept("escape", sys.exit())
else:
print "client quit"
PlayerID = pkg.getString()
self.Models[PlayerID].detachNode()
del self.Models[PlayerID]
del self.PList[PlayerID]
dta = PyDatagram()
dta.addUint16(MSG_CQUIT)
dta.addString(PlayerID)
for Client in self.Clients:
self.cWriter.send(dta, Client)
elif (msgID == MSG_CQUIT): #this message is send from server to client only, when another client has quit
print "client quit"
PlayerID = pkg.getString()
self.Models[PlayerID].detachNode()
del self.Models[PlayerID]
del self.PList[PlayerID]
elif (msgID == MSG_CPOS):
PlayerID=pkg.getString()
if (PlayerID != self.myID):
X=pkg.getFloat64()
Y=pkg.getFloat64()
Z=pkg.getFloat64()
H=pkg.getFloat64()
P=pkg.getFloat64()
R=pkg.getFloat64()
StatList = [X,Y,Z,H,P,R]
self.PList[PlayerID] = StatList
def updateClients(self): #Server Side Function
dta = PyDatagram()
NUM = len(self.PList) #calculate number of records in the data package
dta.addUint16(MSG_POS) #Set Package Type
dta.addUint16(NUM) #Define Package Length
for x in self.PList: #Construct data package
dta.addString(x)
stats = self.PList[x]
num = len(stats)
for y in range(num): #construct a single record
dta.addFloat64(y)
for Client in self.Clients: #send Data package to Clients
self.cWriter.send(dta, Client)
def updateWorld(self, task): #Common Task
for i in self.PList:
print i, self.PList[i]
if self.Models.has_key(i):
if (i != self.myID):
data = self.PList[i]
self.Models[i].setX(data[0])
self.Models[i].setY(data[1])
self.Models[i].setZ(data[2])
self.Models[i].setH(data[3])
self.Models[i].setP(data[4])
self.Models[i].setR(data[5])
elif(i.strip() != ""):
if (i != self.myID):
self.Models[i]= loader.loadModel("models/fighter")
self.CSP[i] = CollisionSphere(0,0,0,15)
self.Cnodes[i] = self.Models[i].attachNewNode(CollisionNode(i))
self.Cnodes[i].node().addSolid(self.CSP[i])
self.Models[i].reparentTo(render)
data = self.PList[i]
self.Models[i].setX(data[0])
self.Models[i].setY(data[1])
self.Models[i].setZ(data[2])
self.Models[i].setH(data[3])
self.Models[i].setP(data[4])
self.Models[i].setR(data[5])
return Task.cont
def updateServer(self): #Client sends his Stats to Server... or Player@Server updates Clients
dta = PyDatagram()
dta.addUint16(MSG_CPOS) #Set Package Type
dta.addString(self.myID)
stats = self.PList[self.myID]
num = len(stats)
for i in range(num):
dta.addFloat64(stats[i])
if (self.SERVER == 1):
print "server sends its position"
for Client in self.Clients: #send Data package to Clients
self.cWriter.send(dta, Client)
else:
print "client sends its position"
self.cWriter.send(dta, self.Connection) #Send Data Package to Server
def quit(self): #Prepare message if you want to quit
if (self.SERVER == 0): #Message if you are client
dta = PyDatagram()
dta.addUint16(MSG_QUIT)
dta.addString(self.myID)
self.cWriter.send(dta, self.Connection)
self.cManager.closeConnection(self.Connection)
sys.exit()
else: #message if you are server
dta = PyDatagram()
dta.addUint16(MSG_QUIT)
for Client in self.Clients:
self.cWriter.send(dta, Client)
self.cManager.closeConnection(self.tcpSocket)
sys.exit()
s=main()
run()
So the program has a cfg file, where it reads the server address, the clients ID and the port.
This file is for convenience, so you won’t have to alter the source code, to test it with some other guys.
The format however is:
myID=345
server=127.0.0.1
port=8500
PS: The secret of the network code is to construct the proper datagram handlers, that’s the only creative part. The rest might be as easy as copy&paste.