In this post, I show how to get an earthquake effect (or screen shaking). As before, OpenGL does most of the job, and there is nearly no overhead!
This post is part of the OpenGL 2D Facade series
I propose different effects; you can see them here:
We use the same vertex shader as before, where we add a shift to all vertices using a uniform variable:
#version 330
layout (location=0) in vec4 vertex;
layout (location=1) in vec2 inputUV;
out vec2 outputUV;
uniform vec4 translation;
uniform vec2 uvShift;
void main() {
gl_Position = vertex + translation;
outputUV = inputUV + uvShift;
}
The final vertex position of each quad/tile is the one of the vertex plus a translation:
gl_Position = vertex + translation;
We set the value of the translation
uniform variable with the Opengl glUniform4f()
function:
translationShaderVar = glGetUniformLocation(shaderProgramId, "translation")
glUniform4f(translationShaderVar, x, y, 0.0, 0.0)
We only consider 2D translations, so there is only an x
and y
value. The shaderProgramId
variable contains the id of our shader.
We still store in two attributes, translationX
and translationY
, the view shift of a layer group. We use them, for instance, to center the world around the player.
We could also change these values to add an effect, but it would be more complex to handle. We would mix a translation that corresponds to some constraints (like centering around the player) with a purely visual effect. It is easier to split these shifts into different attributes and sum them before transferring them to the shader. Each one has its rules, and we can handle them separately.
Consequently, we add a new shiftEffectX
, shiftEffectY
attribute pair in each layer group. Then, during the drawing, we add the shifts (see the render()
method of OpenGLGUIFacade
class):
translationShaderVar = glGetUniformLocation(self.__shaderProgramId, "translation")
uvShiftShaderVar = glGetUniformLocation(self.__shaderProgramId, "uvShift")
for layerGroup in self.__layerGroups:
if layerGroup is None:
continue
x = layerGroup.translationX + layerGroup.shiftEffectX
y = layerGroup.translationY + layerGroup.shiftEffectY
glUniform4f(translationShaderVar, x, y, 0.0, 0.0)
for layer in layerGroup:
if layer is None:
continue
glUniform2f(uvShiftShaderVar, layer.uvShiftX, layer.uvShiftY)
layer.draw()
Finally, when we want to add a shift for effects, we use a new setShiftEffect(x, y)
method in the LayerGroup
class.
As we can see in the video above, there are different ways to shake the screen. In each case, we compute a shift for each frame considering a function. Note that we only compute horizontal shifts, but you can easily extend to vertical ones if you wish.
Let's start with a random function:
f = random.uniform(-1, 1)
x = f * 32
self.__layerGroup.setShiftEffect(x, 0)
We ask for a random value between -1 and 1 (line 1), multiply it by 32 to get shifts up to 32 pixels (line 2), and send that value to the layer group.
To get something more regular, we can use the sinus function:
f = math.sin((time - start) / 25)
x = f * 32
self.__layerGroup.setShiftEffect(x, 0)
The time
variable contains the current time in milliseconds, and start
the time when the effect started. As a result, (time - start)
is the number of milliseconds since the effect started. We divide it by 25 and compute the sinus of the result (line 1). You can change this value to get a different shake rate. The sinus function always returns values between -1 and 1, and sin(0)
equals 0. Consequently, f
starts with a zero value, increases to 1, decreases to -1, comes back to 1, and so on.
We can get a more exciting effect with some fading:
f = ...
m = 1.0 - (time - start) / (end - start)
x = f * m * 32
self.__layerGroup.setShiftEffect(x, 0)
The m
variable goes from 1 to 0. It equals 1 when the effect starts (the current time equals start
), and equals 0 when the effect ends (the current time equals end
).
We implement these shift computations in the RegionRenderer
class in the render
package.
We add new attributes to trigger and manage this effect:
self.__earthquakeType = ""
self.__earthquakeFade = ""
self.__earthquakeStart = 0.0
self.__earthquakeEnd = 0.0
The earthquakeType
attribute defines the effect function: "random" or "sinus" (empty string means no effect). The earthquakeFade
attributes can define a fade function: "linear" or nothing. The two last attributes define the beginning and the end of the effect.
We can set these values with a new startEarthquake()
method in the RegionRenderer
class. You can see examples of calls in the PlayGameMode
class in the mode
package, where we trigger earthquakes when the player presses a key.
The updateAnimations()
method of the RegionRenderer
class handles the computation of the shift:
def updateAnimations(self, time: float, epochFrame: float):
for renderer in self.__renderers:
renderer.updateAnimations(time, epochFrame)
if self.__earthquakeType == "":
self.__layerGroup.setShiftEffect(0, 0)
return
if time >= self.__earthquakeEnd:
self.__earthquakeType = ""
self.__layerGroup.setShiftEffect(0, 0)
return
f = 0
if self.__earthquakeType == "random":
f = random.uniform(-1, 1)
elif self.__earthquakeType == "sinus":
f = math.sin((time - self.__earthquakeStart) / 25)
m = 1.0
if self.__earthquakeFade == "linear":
m = 1.0 - (time - self.__earthquakeStart) / (self.__earthquakeEnd - self.__earthquakeStart)
x = f * m * 32
self.__layerGroup.setShiftEffect(x, 0)
Lines 2-3 are the previous animation handling.
Lines 5-7 leave if there is no effect to run.
Lines 9-12 leave if the effect is over.
Lines 14-23 compute the shift.
In the next post, we create a point light.