Since
Dani's Race Version 2024-09-25 is released. And the petition for
Version 2025-01-19 has begun
( please sign it ), I thought it would be fitting to talk about a very big aspect of the game. Mainly breakable cars.
We all love some mayhem when it comes to playing games. And nothing makes car games more satisfying than damage models. RockStar Games understood it early on, and all GTA games have breakable cars. Today some of the most popular car games
like BeamNG.drive holding on a realism of damage models almost solely. And therefor for me, making
Dani's Race any other way, would have not been a good idea. I knew I had to make the cars in my game breakable.
So how exactly am I doing it?
The right approach
A big part of my early childhood were the extremely brutal R-rated GTA games, which I have a lot of nostalgia for. Those did not go full on deformation simulation that something like Rigs-Of-Rods, or
BeamNG would do. The way cars broke, in those GTA games I have so many fun memories with, was much simpler. And I tried replicating this same approach in Dani's Race.
The car model is actually multiple models put together. One for the body, one for the hood, one for each door. Those models can be replaced with a broken version, or completely detached. Simulating various stages in the car's health. As you can see on the screenshot there, this is exactly what I did for Dani's Race.
Coding the approach
One thing is to prepare models for a car, the other is to make them actually work in the game. The main problem is how do you know which model is being broken and when?
Thankfully
UPBGE had me covered. The
Vehicle.py file contains a function called
OnCollision()
which I could set to be executed on every collision in the following way:
def OnCollisionDo(obj, point, normal, points):
if car["active"] or (InView(car) and Opt.GoodFPS("Collision"),0.6):
OnCollision(car, obj, point, normal, points)
car.collisionCallbacks.append(OnCollisionDo)
Yeah, I stuffed the execution of the function into a wrapper that checks whether the FPS is good or not. The engine
requires some measures sometimes to tame it.
But anyway, you can see that the function receives ( from the engine ) the following data: The car, or the object that is colliding. The
obj
or the object the car is colliding with. The point, or the position of where the collision happens. The normal, or the direction of the collision. And the points. With the "s". Because sometimes during the same frame there could be more than one collision happening in the same time.
The thing is, those point*
s contain the crucial part of the information I was looking for when making the breakable cars.
for point in list(points)[:2]:
force = point.appliedImpulse / car.mass / 2
They have the
appliedImpulse
which is the push the car gets from the collision. It lets me know how hard was the hit. Because trying to estimate it ( which I tried previously using velocity ) wasn't going to work particularly well.
So using all this stuff I know during every collision how hard it is and where it happens. So I can, for example, find the closest object to the collision point on the surface of the car, like the door that just happened to be one that the other car just rammed into, and combine that info into something that will make that door deform.
But it is not all. The doors need to have their own physics.
def SwingDoors(car):
reference = ( car["localLinearForces"] + car.localLinearVelocity ) / 2
for door in car.get("doors",[]):
if door.get("locked", True) or not door.visible:
continue
minrot = door.get("minrot", 0)
maxrot = door.get("maxrot", math.pi/2)
lockat = door.get("lockat", 0)
breakat= door.get("breakat", math.pi/2)
axis = door.get("axis", 2)
factor = door.get("factor", 1.0)
offset = door.get("offset", math.pi)
rot = door.localOrientation.to_euler()
rot[axis] = (math.atan2(*(i for n, i in enumerate(reference) if n != axis )) + offset ) * factor
for n in range(3):
if n != axis:
rot[n] = 0
if door.get("gravity", axis != 2):
if axis == 1: gravityaxis = 2
else : gravityaxis = 1
gravityfactor = reference[gravityaxis]
if gravityfactor < 0: gravityfactor *= -1
gravityfactor = min(30, gravityfactor)/30
gravityfactor = 1 - gravityfactor
rot[axis] = rot[axis]+((lockat - rot[axis])*gravityfactor)
maxrot -= minrot
rot[axis] -= minrot
if rot[axis] < 0: rot[axis] += math.pi*2
if rot[axis] > (maxrot/2)+math.pi:
rot[axis] = 0
elif rot[axis] > maxrot:
rot[axis] = maxrot
rot[axis] += minrot
oldrot = door.localOrientation.to_euler()[axis]
diff = ( rot[axis] - oldrot )
if diff > math.pi: diff -= math.pi*2
elif diff < -math.pi: diff += math.pi*2
rot[axis] = oldrot + ( diff / 5 )
door.localOrientation = rot
if door["health"] > 0.5 and round(rot[axis],2) == round(lockat, 2):
door["locked"] = True
rot[axis] = lockat
if round(rot[axis],3) == round(breakat, 3):
door["health"] -= 0.003
if door["health"] < 0.1:
door.visible = False
newdoor = Reuse.Create(door.name, 500)
if "Borked" in door:
newdoor.replaceMesh(door["Borked"])
newdoor.position = door.position
newdoor.orientation = door.orientation
newdoor.worldLinearVelocity = car.worldLinearVelocity / 1.5
ColorPart(car, newdoor)
As you can see I wrote a whole essay worth of code just to make the various doors swing in the game. I tried originally using the bullet physics that comes with the engine to do the swinging. But the doors didn't behave. So instead I decided to write my own horrible approximation of what the doors should do. Which works most of the time. It is bit a silly, sometimes they over-react to very minor movements of the car. But for the most part this does the job.
Though as you can see, this function needs a lot of stuff from the door. From
minrot
to
maxrot
to things like
lockat
,
breakat
,
factor
,
offset
and so on and so forth. Which I need to manually provide for each door. And it doesn't help my sanity that the rotational math ( because doors swinging are done with rotating them along the hinges ) is done with radians. Don't get me wrong, I like radians. But sometimes looking at a half a pi and comparing it to pi times 2 makes me scratch my head.
And we are not even talking about the whole other complexity outlined in the
Tools.py when it comes to fixing those parts. Because unlike in other games, where you just put the car in the garage and the car in magically fixed, in Dani's Race you need to fix it yourself. Part by part.
Putting the car together for the game
Now because I know what I'm trying to do when making cars for this game, let's actually go over a step by step process of how I take a car from
Moria's Race and make it a car in Dani's Race.
As you probably can tell, I can't simply use the models directly from the film in the game-sequel.
The 2025-01-19 version summary shows exactly why. The versions used for the film are too high resolution. And it will not run well in the game. So the first step is to delete the resolution.
Sometimes, like with
Neonspeedster I'm lucky to have the pre-baked version of the car, from which a proper low-res version could be generated. Other times I need to manually delete unnecessary loops as you can see me doing in this screenshot.
It is a screenshot from a livestream I did, in which I added the
Underground car from the movie. Here is the full recording of it:
As you can see I meticulously dissolved edges that aren't needed. Until I reduced the 150k, or so, triangles of a model to be below 5K triangles, while keeping the shape largely the same.
Then I struggled with rigging it onto the template I had from another car in the game. And setting up all the doors and stuff for breakness. The stream is not full. I did the deformation of the doors off stream. But you can see that a single transformation of a car. Not even making of an original car for a game. But using a pre-existent model, took me above 4 hours to put into Dani's Race. And it was so fast only because I know how to do it. The game has not a small amount of breakable cars by now.
I will remind you again. The version with this car and the new optimization methods will be released only when the petition is over. So please
go sign it now and...
Happy Hacking!!!
How Can I Improve The Website
Unread
Blender Dumbass
👁 73 💬 13
Any suggestion about how I can make the website better
The Pentas
Unread
Blender Dumbass
👁 36 💬 0
Pentas are 5 gong-style round drum-things which are used to play simple melodies. Each one of those is one note in a scale called the Pentatonic scale. If you take the modern western scale with it's 7 notes (Do, Re, Mi, Fa, Sol, La, Si). The pentatonic scale is the best sounding 5 of those (Do, Re, Mi, Sol, La). The easiest way of achieving the pentatonic scale on a piano would be to play only the black keys. Also the pentatonic scale is quite popular with beginner guitarists. It's rather simple on a fret-board and gives a nice sounding solo when improvising. The Pentas - being my second album, was still recorded during my time learning the guitar. So I used a lot of the pentatotic scale in it. Thought quite frankly, knowing about the other two notes (Fa and Si) I added them quite often still.
UPBGE - What is Depsgraph? And How to Optimize for Depsgraph?
Unread
Blender Dumbass
👁 66 💬 0
You see things like "Physics", "Logic" and even "
Rasterizer" and you immediately understand what you need to do to optimize you game. But "Depsgraph"?... It looks like a mysterious thing that nobody knows nothing about. Yet is it one the most problematic things there is in your game. And you are going mad just trying to figure it out.
#DanisRace #MoriasRace #Game #Gamedev #UPBGE #blender3d #animation #GTAClone #programming #python #project #performance #depsgraph
Powered with BDServer
Analytics
Mastodon
PeerTube
Matrix