![Launcher [embedded image]](/pictures/user_upload/blenderdumbass/dr_code_explained.png)
import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk
gi and make it set the version of GTK to specifically 3.0. Any new system with any newer GTK ( if I don't that ) will try to run the code on the newest version of GTK. And they change a lot between versions. If I remember correctly even the concept of Window was basically removed in newer versions.
↩ Reply
win = Gtk.Window() win.set_title("Dani's Race") win.set_size_request(799, 280) win.connect("destroy", Gtk.main_quit) win.set_default_icon_from_file("SettingUI/icon.png") box = Gtk.VBox() win.add(box)
SettingUI folder.
↩ Reply
win.show_all() Gtk.main()
Gtk.main() loop. This function inside of the GTK module doesn't stop running until Gtk.main_quit() is executed. But fortunately I already connected this function to the window's destroy event. Which means if you close the Launcher window, it will run Gtk.main_quit() stopping the GTK loop and exiting from the program.
↩ Reply
playbutton = Gtk.Button("🎮 Single Player") playbutton.connect("clicked", play) playbox.pack_start(playbutton, 0,5,5)
play(). So what does this play() do?
↩ Reply
def play(w, multiplayer=False): data = load_settings() data["multiplayer"] = multiplayer save_settings(data) if data: blenderplayer = data.get("blenderplayer", "Engine/blenderplayer") try: os.system("chmod +x "+blenderplayer) except: pass command = ['stdbuf', '-o0', blenderplayer] if data.get("fullscreen", True): command.append("-f") else: command.append("-w") command.append(data.get("resolution", "1920:1080").split(":")[0]) command.append(data.get("resolution", "1920:1080").split(":")[1]) # The file of the game command.append(os.getcwd()+"/assembly.blend") process = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True)
def play(w, multiplayer=False): data = load_settings() data["multiplayer"] = multiplayer save_settings(data)
play() receives 2 arguments: w and multiplayer which is by default set to False.
↩ Reply
w is reserved for the GTK widget that calls this function. If I don't add anything to catch that, it will just error out. So I add the w variable. And then disregard it. ( It could be useful in other circumstances though ).
↩ Reply
multiplayer is used to set whether this game is played in multiplayer or not.
↩ Reply
multiplayer to the data in that file, and then saves the settings. When the game itself loads, it reads the settings file to apply all of the user-defined settings. But also it reads the multiplayer variable, to know whether to connect to a server or not. Or whether to execute certain multiplayer related functions in the game itself or not.
↩ Reply
blenderplayer = data.get("blenderplayer", "Engine/blenderplayer")
blenderplayer location.
↩ Reply
blender and blenderplayer. Blender looks like your standard blender. blenderplayer on the other hand has no UI what so ever. blenderplayer is used to directly run a UPBGE game. To run a UPBGE game you give blenderplayer a gamefile and that is all you need to do, technically.
↩ Reply
try: os.system("chmod +x "+blenderplayer) except: pass
blenderplayer file an executable. It is needed to do here, because we don't know who will install the game. And how much they know about anything. It should just run.
↩ Reply
command = ['stdbuf', '-o0', blenderplayer] if data.get("fullscreen", True): command.append("-f") else: command.append("-w") command.append(data.get("resolution", "1920:1080").split(":")[0]) command.append(data.get("resolution", "1920:1080").split(":")[1]) # The file of the game command.append(os.getcwd()+"/assembly.blend") process = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True)
subprocess module. But as you can see it doesn't just run it.
↩ Reply
stdbuf -o0 to the beginning of the command. This is important for later.
↩ Reply
<ul><li>f or -w to the command. You can pass various arguments to the blenderplayer executable to change how the game is launched. -f launches the game in full-screen mode. -w launches the game in windowed mode.
↩ Reply
subprocess.Popen.
↩ Reply
stdbuf all about? Well... the launcher doesn't just runs the game-engine. It also spawns a second thread to listen to the game-engine's output ( the stdout stuff ). stdbuf -o0 stuff helps the stdout be a little bit more real-time. Without it the output would be slow and would be received in large chunks.
↩ Reply
![progress bar [embedded image]](/pictures/user_upload/blenderdumbass/dr_code_explained1.png)
GAME STARTS ) is dumped into a file:
↩ Reply
GAME STARTS to stop writing stdout into grabbed.txt. All of the stuff before that, are various warning that UPBGE talks about. And various messages like:
↩ Reply
Info: Read library: '/home/vcs/Projects/Games/DanisRace/ast/veh/NeonSpeedster.blend', '//ast/veh/NeonSpeedster.blend', parent '<direct>'
NeonSpeedster.blend at some point.
↩ Reply
grabbed.txt of the previous launch and stdout of this launch, we can estimate, how far we are into actually starting the game itself. And using this estimation we can render a little progress bar.
↩ Reply
~/.local/share/danisrace/config.json
↩ Reply
json module ).
↩ Reply
load_settings() function which looks like this:
↩ Reply
def load_settings(): folder = get_settings_folder() f = folder+"/config.json" try: with open(f) as o: return json.load(o) except: try: with open("default_config.json") as o: return json.load(o) except: return {}
get_settings_folder() which looks like this:
↩ Reply
def get_settings_folder(): game = "danisrace" try: data_dir = os.environ["XDG_DATA_HOME"] + "/" + game except: data_dir = os.path.expanduser("~/.local/share/"+game) try: os.makedirs(data_dir) except: pass return data_dir
XDG_DATA_HOME environment variable.
↩ Reply
~/.local/share/danisrace. Obviously I'm passing it through os.path.expanduser because python directly doesn't understand that on my machine ~ is actually /home/vcs.
↩ Reply
load_settings() can use said folder to try to load config.json out of it using standard json stuff.
↩ Reply
![Settings dialog [embedded image]](/pictures/user_upload/blenderdumbass/dr_code_explained2.png)
config.json file. It is implemented in the standard GTK with standard GTK widgets. As of lately I started separating various settings into subgroups using a Gtk.Notebook().
↩ Reply
configs contains 5 different graphical presets that can be auto-applied using the first menu-bar in the UI. Technically speaking, adding more files into configs will give you more options to choose from. In case you want custom presets.
↩ Reply
"config_name":"<your config name>",
![Multiplayer window [embedded image]](/pictures/user_upload/blenderdumbass/dr_code_explained3.png)
client.py module:
↩ Reply
{"games":null} for example, will be a request to get the list of active games. The JSON is then encoded into utf-8 and compressed using zlib module, before sending.
↩ Reply
def encode(data, send_raw=False): # This will encode data for sending. # Data should be a writable into JSON. if not send_raw: J = json.dumps(data).encode("utf-8") else: J = data.encode("utf-8") C = zlib.compress(J) zed = len(C) raw = len(J) # Sometime compressed is larger than raw. # We will add a byte saying 69 if it comressed # and a byte saying 0 if it not sexy like this. if not send_raw and zed < raw: return chr(69).encode("utf-8") + C else: return chr(0).encode("utf-8") + J
encode() function checks for which version is smaller. And puts the binary of either 0 or 69 ( nice ) in from of the message, to tell the other side which version this is.
↩ Reply
play() function that I already explained earlier. But this time tells it to set multiplayer to True. Meanwhile the UI of the multiplayer itself is just a fancy editor for the same old config.json. This time adding parameters like which server and which game you are trying to play.
↩ Reply
notgit.py commit on my computer.
↩ Reply
Commit() function inside of notgit.py.
↩ Reply
.notgit/commits folder.
↩ Reply
DanisRace folder, on which I'm developing. Straight from the computer, on which I am doing the development. Also any link to any of the files in Dani's Race in this article is also linked directly to the file on my computer. I'm using the same system for this article.
↩ Reply
md5 hash. Those are rather small. Here is the hash for the current run.py:
↩ Reply
f61361b296062cc672427dc40f692588
md5 hashes. And you can check your list of files and your list of hashes against my hashes. And if any of the hashes don't match, the file must be changed. And only then you need to download the entire thing.
↩ Reply
![Score board [embedded image]](/pictures/user_upload/blenderdumbass/dr_code_explained4.png)
danisrace_api.py plugin on blenderdumbss.org server.
↩ Reply
![Assembly blend [embedded image]](/pictures/user_upload/blenderdumbass/dr_code_explained5.png)
assembly.blend file. Everything in this file is the game. But because blender can do that, and because it is convenient, some of the stuff visible in assembly.blend is actually linked from a different file.
↩ Reply
ast folder for example, contains various 3D models. All different .blend too, which are linked into assembly.blend.
↩ Reply
loc folder in the ast which contains a version of the city of the game. At one point the city itself was linked as well. But a decision was made to make the city a direct part of assembly.blend. It is way more convenient to work on it like that.
↩ Reply
ast/Shared_Materials.blend. This files contains materials that are often used between other ast files. Like Glass and Rubber, used in pretty much every car in the game. Because all those materials are linked from the same source, the game engine knows it is the same material and doesn't waste draw cycles rendering them all separately.
↩ Reply
![Main Update [embedded image]](/pictures/user_upload/blenderdumbass/dr_code_explained6.png)
main() function in that file.
↩ Reply
Scripts.Main_update.main() on every frame of the game ( or "Always" ). But as you can see sensors for mouse movement and mouse-wheels are also connected to the same script.
↩ Reply
Scripts.Vehicle.py there is the UpdateCar() function. For NPC characters, there is the Scripts.Character.Update() function. And specifically for Dani ( the main character ) there is Scripts.Character.DaniUpdate() function.
↩ Reply
if car.get("active"): UserControl(car)
bge module that let's your python scripts read and alter game states.
↩ Reply
KX_GameObject has a built-in python dictionary. So you could assign variables to said object directly. Say I have a car representing a vehicle in the game-world. I can assign to it things like car"active"
↩ Reply
car.get("active") returns True, we run the UserControl() function, which in turn, let's the user control said car.
↩ Reply
![neonspeedster [embedded image]](/pictures/user_upload/blenderdumbass/dr_code_explained7.png)
Box object which runs the UpdateCar() function. In the Neonspeedster, for example the Box object is called NeonSpeedsterBox.
↩ Reply
Box object is an actual rigid-body physics object in the game. But as you can see in the screenshot, it is rather simple in shape. Those Box objects all use Convex Hull bounds. This means that the car is not a square or a ball, but roughly shaped as a car. So when you flip it over, it will flip more or less properly.
↩ Reply
Box various render-able objects are attached. Mainly the Car_body of the car. This Car_body is called exactly the same in each car asset. It is possible because those assets are linked in whole into the assembly.blend. The problem is you cannot simply find said Car_body by just looking at the list of objects in the scene. What you need to do instead looks something like this:
↩ Reply
body = car.childrenRecursive["Car_body"]
Car_body object is parented to the Box object, meaning it is in the children of the Box object. And if you only iterate over the children, you can find the proper Car_body.
↩ Reply
Car_body object to the side of the Box object. This broken version has a small logic brick setup basically making it invisible as soon as the car spawns into the game.
↩ Reply
health level, the code runs the ChangeBody() function, which replaces the mesh-data of the current Car_body with the data in the broken version. The broken version is there just to hold the data. It never actually gets revealed.
↩ Reply
Neonspeedster-Door_Pease.001. This is because new parts of this exact model need to be spawn-able at will, when the car "detaches" parts. On every detach, the part just becomes invisible, while a new part of the same exact model is spawned in its location. If I would call them more generic names like Door_R or something, I run a risk of a door from one car spawning in for another car which would look silly.
↩ Reply
![Car door settings [embedded image]](/pictures/user_upload/blenderdumbass/dr_code_explained8.png)
minrot, maxrot, Good, Borked and so on and so forth. Vehicle.py has a function called SwingDoors() that uses those values to simulate physics on those doors.
↩ Reply
minrot and maxrot variables control the range of rotation. You can add things like axis to the variable list, if you want it to wobble not on the X axis, like for hoods and trunk-doors.
↩ Reply
Dani_Stand_Target which is used for the animation of entering the car. This is the position next to the car that marks the driver's door. You also have Dani_Sit_Target, which is the point in the car, where the model of Dani would be parented to, when he is sitting in the car.
↩ Reply
Other_Sit_Target and Other_Stand_Target.
↩ Reply
SpoilerProxy and NitroCanProxy that are used by the PowerWrench look logic, to attach Spoilers and Nitro into the car. Those mark the target position, rotation and scale of those attach-able objects.
↩ Reply
cardata folder, like the:
↩ Reply
{ "name":"Neonspeedster", "probability":1, "type":"race-car", "material":{"MainColor" :[0.051525, 0.514757, 1.000000], "SecondaryColor":[0.051525, 0.514757, 1.000000]}, "icon":[2,0], "cost":2990000, "wheels":[ { "radius":0.42, "xyz":[ 0.89, -1.25, 0.150], "down":[0,0,-1], "axel":[-1,0,0], "suspensionRest": 0.2, "front":true , "object":"" , "drift_grip":0.2}, { "radius":0.42, "xyz":[-0.89, -1.25, 0.150], "down":[0,0,-1], "axel":[-1,0,0], "suspensionRest": 0.2, "front":true , "object":".001", "drift_grip":0.2}, { "radius":0.42, "xyz":[-0.89, 1.65, 0.150], "down":[0,0,-1], "axel":[-1,0,0], "suspensionRest": 0.2, "front":false, "object":".003", "drift_grip":0.1}, { "radius":0.42, "xyz":[ 0.89, 1.65, 0.150], "down":[0,0,-1], "axel":[-1,0,0], "suspensionRest": 0.2, "front":false, "object":".002", "drift_grip":0.1} ], "mass" : 1800 , "hp" : 600 , "torque" : 120 , "drag" : 0.25 , "roll" : -0.4 , "grip" : 0.3 , "maxturn" : 0.8 , "brakes" : 2.0 , "abs" : 0.01 , "downforce" : 2.0 , "suspention" : 200.0 , "suspentionDamping" : 0.6 , "maxrpm" : 8000 , "idle" : 1100 , "redline" : 6000 , "gears":[ 5.00, 3.00, 2.00, 1.00, 0.75, 0.50, 0.25, 0.20, 0.15 ] }
UpdateCar() function. Let's digest that:
↩ Reply
def UpdateCar(car): # This function runs on every frame for each car, # making the car interactable in the game. # Clear the car and get the scene scene, car = GetSceneAndCar(car) dani = scene.objects["Dani_Box"]
car) is not the KX_GameObject of the car itself, but rather a bge.types.SCA_PythonController object representing the logic-brick that calls the function. Now sometimes I just want to run UpdateCar() on an actual KX_GameObject. But most of the time I will get the SCA_PythonController. This is where the GetSceneAndCar(car) function helps us.
↩ Reply
def GetSceneAndCar(car): # This module parses the inputs of other modules. # And gives back bge.scene and the car object. # We don't know if what we get here is the controller # where the script is running, or the object itself. if type(car) == bge.types.SCA_PythonController: car = car.owner scene = bge.logic.getCurrentScene() return scene, car
carD = Opt.FastDict(car)
KX_GameObject is not actually a real python dict but only pretends like one for convenience, it is a bit slower to assign values directly to the game object. Opt.FastDict() is giving me a real python dict specifically associated with the car. Into which I can put values that are constantly changing.
↩ Reply
Opt module a bit later, when talking about Optimization.
↩ Reply
PhysicsUpdate() function. Which does this check:
↩ Reply
initfirst = False if not getCarPhysics(car): InitCarPhysics(car) initfirst = True
getCarPhysics() is a very simple function that basically tries to give you the physics constraint of the car. But if fails to do so ( like when the car is new and just spawned in ) it give you False, prompting the InitCarPhysics() to run.
↩ Reply
InitCarPhysics() is actually quite interesting.
↩ Reply
def InitCarPhysics(car): # This part of code runs once per car. It generates the # Bullet Physics Vehicle Constraint setup. # Clear the car and get the scene scene, car = GetSceneAndCar(car) carD = Opt.FastDict(car) # Get the data about this car car["specs"] = GetCarSpecs(car.get("carData", car)).copy() # Create a constraint carPhysics = bge.constraints.createVehicle(car.getPhysicsId()) car["cid"] = carPhysics.getConstraintId() car["VehicleConstraintId"] = carPhysics.getConstraintId() # Adding wheels for n, wheel in enumerate(car["specs"]["wheels"]): AddWheelToCar(car, wheel) car["wheels"][n].position = carPhysics.getWheelPosition(n) # Connect doors and other hinged objects car["doors"] = [] for obj in car.childrenRecursive: if obj.get("door"): car["doors"].append(obj) obj.suspendPhysics() # Init Various things we might need # Engine things carD["rpm"] = float(0.0) carD["turn"] = float(0.0) carD["prevF"] = float(0.0) carD["prevT"] = float(0.0) carD["health"] = float(1.0) carD["nitro"] = float(0.0) carD["shake"] = float(0.0) # Set the mass of the car. We divide the mass # by 10, since there is a limit of how strong # can be car's suspension. UPBGE does not provide # a way ( yet ) to override this limit. car.mass = car["specs"]["mass"] / 10 # Create a callback for collisions def OnCollisionDo(obj, point, normal, points): # For readability reason, I put the code # for the task itself in a separate funtion # below. if car["active"] or (InView(car) and Opt.GoodFPS("Collision"),0.6): work_anyway = False if obj == "Explosion": work_anyway = True obj = {} OnCollision(car, obj, point, normal, points, work_anyway) # I would love to be able to do a Scheduling instead of simple # FPS checking for non player cars. But this results in Seg-Faults # because apperately the script tries to read "points" that are # freed after the simulation step of the collision is over. car.collisionCallbacks.append(OnCollisionDo) # For trucks UnloadCargo(car) car.angularVelocityMax = car["specs"].get("maxAngularVelocity", 1.5) print(consoleForm(car),clr["bold"]+clr["tdrd"],"Physics Initilized.", clr["norm"])
bge.constraints.createVehicle() function that is built into UPBGE. UPBGE internally runs Bulled Physics as the physics engine, which contains a Vehicle Constraint functionality. And so I can use that Vehicle Constrain functionality to make my vehicles.
↩ Reply
car"specs"
↩ Reply
car.collisionCallbacks.append(OnCollisionDo) function.
↩ Reply
OnCollision() function that does quite a lot of work. For instance, it is the function that handles the car deformations. It is also the function that starts a pursuit mode, when you hit NPC cars on the street.
↩ Reply
PhysicsUpdate(), which internally calls other things, like ApplyEngine() and other stuff.
↩ Reply
Vehicle.py is full of little function that do various small things. Look at this one for example:
↩ Reply
def HorsePowerToWatts(value): return value * 745.6998715823
carD ) has a variable npc which you can set to make it do various things.
↩ Reply
if car.get("active"): UserControl(car) elif carD.get("npc") == "npc": NormalNPC(car) elif carD.get("npc") == "pursuit": AngryPursuit(car) elif carD.get("npc") == "autopilot": TargetedNPC(car) elif carD.get("npc") == "goingToRace": TargetedNPC(car) Map.Show(car, color=GetColor(car)) elif carD.get("npc") == "taxiComing": TargetedNPC(car) Map.Show(car, color=GetColor(car)) elif carD.get("npc") == "story": pass # If it is controlled from story elif carD.get("npc") == "race_ghost": RacingGhost(car) elif car.get("racing"): RacingAI(car)
NormalNPC(), TargetedNPC(), RacingAI() and AngryPursuit() for example use targeting, which is implemented similarly. Just they go to different targets and using slightly different logic to achieve their goals more optimally. For example NormalNPC() will go to random points on the map, to simulate a car with a destination, while TargetedNPC() will have a predefined destination and will use path-finding to get to the destination. On the other hand RacingAI() and AngryPursuit() will disregard safety and try to get to their respective targets as fast as possible.
↩ Reply
Encode() and Decode(). Those save and restore a state of a car. Say the player puts a car into his garage, at home and then closes the game. You want this car to be saved for when the player opens the game next time. The Encode() function dumps all the relevant data about the car into a JSON-like structure ( which is easy to dump into JSON ). Decode() takes this JSON-like structure and actually makes a real game object of a car. This is also useful ( and used ) in the multiplayer. That is why, you can see, that Decode() function is very selective about how to decode the car. You don't want to send to the server all of the data all the time. And therefor something you may need to decode only some of the data.
↩ Reply
![character [embedded image]](/pictures/user_upload/blenderdumbass/dr_code_explained9.png)
Box object which is the physics objects that is interacting with the world. They also have the Body that represents the character visually. And that also can change shapes ( outfits ). It is very similar to how the car has deformed and un-deformed options. But this time with more than 2 options.
↩ Reply
![actions [embedded image]](/pictures/user_upload/blenderdumbass/dr_code_explained10.png)
ApplyAnimation() from Character.py handles everything for me.
↩ Reply
ApplyAnimation(dani, "Walk") ApplyAnimation(dani, "AnswerPhone")
AnswerPhone is set in the json:
↩ Reply
"AnswerPhone":{ "Action":"DanisPhone", "Frames":[32, 100], "kwargs":{"layer":2}, "Keep":true },
"kwargs":{"layer":2}, is that does the magic here. We are overlaying DaniWalking action with DanisPhone action, putting the phone action in layer 2. Meaning they can play together.
↩ Reply
personality. Look at Update() function to see what I mean.
↩ Reply
personality is a way for me to program the NPC's train of logic. Here is an example:
↩ Reply
driver = car.get("targetDriver") officer = car.get("driver") police = [139, -252.3, 40.6] prison = [81.58, -242.6, 50.41] action = ["wait", 30, ["grab", driver, ["say", "police_miranda", ["say", "police_incar", ["drive", car, police, ["grab", driver, ["walk", "Police_Path", "important", ["put", prison, ["walk", "Police_Path", "important"] ] ] ] ] ] ] ] ]
Scripts.Vehicle.AngryPursuit() function. When the police finds out that they can try to arrest Dani on foot, they execute this set of instructions.
↩ Reply
grab, say, drive, etc... And the last thing on the list, is the next task to do, which is a list in-itself. In between are the arguments of the task. "grab", driver,` kind of makes sense. Then `["say", "police_miranda"` kind of also makes sense. It's just the way I decided to program it looks a bit stupid lisp-like.
Update() function in Character.py actually goes over all this nonesense and actually executes it.
↩ Reply
Opt module:
↩ Reply
GoodFPS() is used to schedule execution of heavy operations. It does some analysis of the operations at hand. Let's them run a little towards the beginning of the game. And records how badly they impact the performance. Then it uses this data to prevent very bad function from running often, trying to massage functions into the available time, to try to hit the target FPS.
↩ ReplyAddress(), UpdateScene() and Precalculate() are used to do dynamic spawning and despawning of decorative dynamic or heavy objects, like breakable wood-fences. Address() is used to not try and measure distances to every single point where a wood-fence could be. But rather separate everything into chunks ( using Precalculate() ) and only check distances to objects within the same Address as the camera.
↩ ReplyFastDict() gives each game-object in the game an assignable dict that works a bit faster than the built-in UPBGE one.
↩ ReplyReuse module:
↩ Reply
Box which has a Vehicle constraint. Where those doors end up is calculated though this dependency graph system.
↩ Reply
Reuse module abstracts this idea of moving objects around into two functions Reuse.Create() and Reuse.Delete() which I use constantly, everywhere in the code.
↩ Reply
Reuse.Delete() and instead of deleting it, the object just moves far away from the map and becomes invisible. I run Reuse.Create() and I get that far-away object. Obviously if I don't have what I need it will actually need to spawn new objects. And sometimes, you will need to actually delete some objects. But for the most part it just moves them arround.
↩ Reply
![navigation mesh [embedded image]](/pictures/user_upload/blenderdumbass/dr_code_explained11.png)
dict on the first frame of the game with Scripts.Navigation.PrecalculateNavigationMesh()
↩ Reply
bpy module which allows me to dig through every single vertex and edge on this navigation mesh. And then record all of their positions somewhere.
↩ Reply
Scripts.Navigation.GetPath() which is my own personal implementation of A* algorithm.
↩ Reply
![minimap [embedded image]](/pictures/user_upload/blenderdumbass/dr_code_explained12.png)
git for this game. Instead, most changes will be recorded via the notgit.py program I wrote for fun the other day.
![[thumbnail]](/pictures/user_upload/Troler/NGWJ6K0NF96N3RYQ.png)
Troler![[thumbnail]](https://forg.madiator.cloud/BlenderDumbass/DanisRace/raw/branch/main/SettingUI/banner.jpg)