Ever taken a trip up a mountain or been on a flight and felt your ears pop? If yes, then you've experienced a phenomenon that's central to our planet - the variation in atmospheric pressure. As we ascend in altitude, the air around us becomes less dense, leading to decreased air pressure. Conversely, as we descend, the pressure increases. This phenomenon isn't just important for weather predictions or aviation but can also serve as an exciting concept for a game!
Imagine a game world where characters move up or down based on real-world air pressure readings. As you physically ascend or descend in the real world, in-game characters would respond similarly. Combining real-world physics with gaming offers not just entertainment but also a unique way to understand and appreciate the intricacies of our environment.
And that's precisely what we're going to explore today! Dive in as we use air pressure to steer our game character, integrating real-world science into a fun and engaging gaming experience. By leveraging sensor data and some clever coding, we'll create a game where pressure changes drive the gameplay dynamics, blending reality with the virtual in an exciting way. So, buckle up and get ready for a pressurized ride into the gaming universe!
Embark on an exciting journey to design a game controlled by vertical hand movements. Capture these swift motions and transmit data wirelessly to the central game station. Power this interactive adventure using Python and redefine known inputs of gameplay.
- Initially, the game will calibrate.
- Once calibrated, the game can be started with the 's' key.
- The character moves up or down based on the air pressure data obtained from the controller.
- Clouds spawn from the right and move towards the character.
- If the character collides with a cloud, the game ends.
- The objective is to avoid the clouds and get a high score.
- The higher the score the faster the clouds move and spawn.
2 DPS310Kits2Go are connected to a computer and both are simultaneously serially transmitting their air pressure readings.
These two kits are set as follows:
- station (placed on a reference surface)
- Dynamic Controller (Held in the hand of the user)
The computer determines the "delta, " which is the difference between the pressures measured at the station and the dynamic controller. This delta serves as an offset, with upper and lower thresholds set to identify the vertical movement of the dynamic controller.
By changing these thresholds you can achieve a larger/smaller sensitivity to motion, however with higher sensitivity to motion comes higher sensitivity to noise. (More on these thresholds below.)
This setup allows maximum noise negation as only the difference delta between both readings of the sensors is taken into account.
Required LibrariesIn this project, a selection of libraries plays a crucial role in orchestrating its functionality. Foremost among these is Pygame, a widely celebrated library in the Python ecosystem, dedicated to game development. Pygame facilitates everything from rendering graphics and detecting collisions to managing in-game events and displaying on-screen text. Complementing Pygame's capabilities, the serial library is leveraged to establish communication with hardware devices. It allows the game to fetch real-time data from serially connected devices, integrating physical motion into the gameplay. Additionally, Python's built-in libraries, such as sys and random, aid in system-specific functionalities and introduce randomness into the game respectively.
Serial Communication between microcontrollers and ComputerFor serial communication, we utilize the 'serial' Python module.
import serial
Initialization of Serial Ports:
base_station = serial.Serial('COM29', 9600, timeout=1)
dynamic_controller = serial.Serial('COM31', 9600, timeout=1)
We already established in advance that the COM ports for the base station and dynamic controller are respectively COM29 and COM31. So all we had to do was to write these two lines in order to inform our code.
def get_float_from_port(port):
"""Try reading a line from a port and convert to float."""
try:
line = port.readline().decode().strip()
return float(line)
except ValueError:
print(f"Couldn't convert '{line}' to float.")
return None
- port.readline(): This reads a line from the serial port until a newline character is found. It returns the line as bytes.
- decode(): This decodes the bytes into a string using a default UTF-8 encoding.
- strip(): This removes any leading or trailing white spaces or newline characters from the string.
Note: Do not directly add the get_float_from_port() function into your game loop because then your code flow will be limited by the times at which you receive serial messages from the microcontrollers. Instead, execute the function in an if condition that checks if there is data available on the serial bus as follows:
if (base_station.in_waiting > 0) and (dynamic_controller.in_waiting > 0) :
base_value = get_float_from_port(base_station)
controller_value = get_float_from_port(dynamic_controller)
CalibrationAir pressure changes in the room are dependent on more than one factor so to ensure a smooth operation of our game and that our controls stay accurate we always have to recalibrate our pressure delta value.
For the game to run and the characters to appear the firstCalibrationFlag has to be set to True. This ensures that calibration is performed at least once before gameplay starts.
if (counterFirstCalibration<10):
counterFirstCalibration+=1
bufFirstCalibration.add(int(controller_value - base_value))
if(counterFirstCalibration==10):
calibratedOffset=bufFirstCalibration.average()
upperThreshold = calibratedOffset + 2
lowerThreshold = calibratedOffset -2
firstCalibrationFlag=True
This code snippet runs right after booting up the game it essentially captures 20 measurements (10 from each microcontroller), performs the delta calculation and takes the average of them. The user also has the option to recalibrate by pressing the keyboard button 'a'; this resets the counterFirstCalibration variable to 0.
The delta value is calibrated at the neutral position: i.e. both the sensor kits should be placed on the same surface.Characters
Sprites are like the actors in a video game, representing characters or objects. Using them makes coding games more organized. They help in easily checking for overlaps or collisions, grouping similar objects together, creating animations, and ensuring the game runs smoothly. Think of them as the building blocks that make both designing and playing games seamless.The DPSMan Class
- Initialization Method (init)
- super().init(): This calls the initialization method of the parent class (pygame.sprite.Sprite), ensuring that all the built-in properties and methods of the Sprite class are inherited.
- self.image: Loads the image of the character using pygame.image.load(). The convert_alpha() method ensures that the alpha channel (transparency) of the image is preserved.
- self.image=pygame.transform.scale_by(self.image, 0.3): This scales the image by 30%, making the sprite smaller.
- self.rect: Retrieves a rectangular area from the image which will be used for positioning, collision detection, etc. It's set to appear in the middle top portion of the window.
- self.mask: Creates a mask for the image. Masks are used in Pygame for pixel-perfect collision detection.
- collisionsWithClouds Method: This method checks if the DPSMan sprite (or character) collides with any sprite in the cloudGroup.
- pygame.sprite.spritecollide(): This function checks for collisions between a sprite and a group of sprites. The method returns a list of all Sprites in a Group that intersect with another Sprite. The pygame.sprite.collide_mask argument means that the method uses the mask for collision detection. If a collision is detected, it returns True.
- Initialization Method (init): Like with DPSMan, this class also inherits from pygame.sprite.Sprite. self.image: Loads the cloud image. self.image=pygame.transform.scale_by(self.image, 0.8): Scales the cloud image by 80%.
- self.rect: Gets the rectangular area of the cloud image and sets its position based on the pos argument provided when creating a cloud instance.
- self.mask: Generates a mask for the cloud, for pixel-perfect collision detection.
- self.pos: Initializes the position of the cloud based on the rectangle's center. This is important for smooth animations.
- self.direction: Sets the movement direction of the cloud. In this case, it's set to move leftwards (-1, 0).
- self.speed: Sets the movement speed of the cloud.
- The update method is called every game loop to update the cloud's position. The position of the cloud (self.pos) is updated based on its direction and speed. The dt factor ensures that movement is consistent regardless of how fast the loop runs. If the cloud moves out of the screen (its right edge is less than 0), it gets destroyed (or 'killed') to save memory and processing.
In the world of video game design, knowing when two objects meet or "collide" is crucial. Pygame offers two primary methods for detecting these collisions: the straightforward rectangle-based approach and the more detailed pixel-perfect method using masks. Here's a quick comparison:
Rectangle-based (Without Masks):
- Objects have invisible boxes (rectangles).
- Collision = boxes overlap.
- Fast but less accurate for irregular shapes.
Pixel-perfect (With Masks):
- Objects have detailed outlines (masks).
- Collision = masks overlap.
- More accurate, but slightly slower.
Mask: A mask is a precise representation of an object's shape, used to detect detailed collisions by checking which specific pixels of one object overlap with another.
The actual collision detection is performed in the collisionsWithClouds method of the DPSMan class:
def collisionsWithClouds(self):
if pygame.sprite.spritecollide(self, cloudGroup, True, pygame.sprite.collide_mask):
return True
- pygame.sprite.spritecollide(...): This function checks for collisions between a sprite (the DPSMan in this case) and a group of sprites (the cloudGroup).
- The True argument indicates that when a collision is detected, the collided sprite in the group should be removed (or 'killed'). This means a cloud will disappear upon colliding with the DPSMan.
- pygame.sprite.collide_mask: This is a collision detection function that checks for collisions using masks (pixel-perfect collision detection). So, instead of just checking if the rectangles of the sprites overlap (which can be imprecise), it checks if the actual pixels (or filled parts) of the sprites overlap, ensuring a much more accurate collision detection.
When a collision is detected, the collisionsWithClouds method returns True, indicating that the DPSMan character has collided with a cloud.
Spawning the CloudsThe cloud creation mechanism is event-driven, relying on Pygame events. In particular, a timer event is set up to initiate the appearance of the clouds.
Custom Timer Event:Firstly, a custom event type for the cloud spawning timer is created:
cloudTimer = pygame.event.custom_type()
Setting up the Timer:Then, the timer is set to trigger the cloudTimer event every 5000 milliseconds (or 5 seconds):
pygame.time.set_timer(cloudTimer, cloudSpawnTimer)
This means that every 5 seconds, a cloudTimer event will be added to the Pygame event queue.
Handling the Timer Event:Inside the main game loop, events are continuously polled and checked. Whenever the cloudTimer event occurs, a new cloud is spawned:
for event in pygame.event.get():
if gameMode:
if event.type == cloudTimer:
cloudClass((2200, randint(0, 1080)), score * 50 + 50, cloudGroup)
cloudSpawnTimer-=300
if(cloudSpawnTimer<600):
cloudSpawnTimer=500
pygame.time.set_timer(cloudTimer,cloudSpawnTimer)
if(score>20):
cloudClass((2200,randint(200,800)),score*80 +100, cloudGroup)
if(score>35):
cloudClass((2200,randint(200,800)),score*80 +100, cloudGroup)
if(score>50):
cloudClass((2200,randint(200,800)),score*80 +100, cloudGroup)
When the cloudTimer event is detected, the cloudClass is instantiated, effectively creating a new cloud sprite:
- (2200, randint(0, 1080)) sets the initial position of the cloud. The X-coordinate is 2200 (presumably off-screen to the right, so it moves into the view), and the Y-coordinate is randomly generated between 0 and 1080 to give vertical variety to the spawning.
- score * 50 + 50 is the speed parameter. As the score increases, clouds move faster, making the game progressively harder.
- cloudGroup is the sprite group to which the cloud sprite is added. Storing sprites in groups allows for easier batch processing of sprites, for operations such as drawing, updating, or collision checking.
- CloudSpawnTimer variable is decremented so that the period between the timer events is also decremented, making the game play more difficult.
- Cloud Spawning Period Limitation: As the score keeps rising the period between timer events is reduced, however the period is not to be set below 0.5 seconds.
- Amounts of Clouds spawned: As the score grows higher, the number of clouds spawned per time event rises up to 4 clouds per timer event.
Inside the cloudClass definition, there's an update method that ensures that if a cloud goes entirely off the screen to the left (i.e., its right edge (self.rect.right) is less than 0), it gets removed (or 'killed'):
if self.rect.right < 0:
self.kill()
This is a good optimization. By removing sprites that are no longer visible or needed, you reduce the computational work and memory usage.
DPSMan MotionData Retrieval for Motion:The motion of the DPSMan character is determined based on the difference (delta) between values read from two serial ports, base_station and dynamic_controller. This difference determines whether the character should move up, move down, or stay in its position.
if base_value is not None and controller_value is not None:
delta = int(controller_value - base_value)
Setting Motion Flags:The character's motion is driven by two flags: flagUp and flagDown. Based on the value of delta, one of these flags is activated:
- If delta is greater than the upperThreshold, flagDown is set to True, which will move the character down.
- If delta is less than the lowerThreshold, flagUp is set to True, which will move the character up.
- If delta is in between these thresholds, both flags are set to False, and the character remains stationary.
The character's position is updated based on the flags:
if hero.rect.top > 10 and flagUp:
hero.rect.top -= 4
if hero.rect.bottom < 1080 and flagDown:
hero.rect.bottom += 4
- If flagUp is True and the character's top edge is more than 10 pixels from the top of the screen, the character moves up by 4 pixels.
- If flagDown is True and the character's bottom edge is less than 1080 pixels from the bottom of the screen, the character moves down by 4 pixels.
This motion logic allows for the vertical movement of the DPSMan character based on the difference in values read from the two serial ports. It ensures that the character does not move off the screen while providing a mechanism for player interaction.
ScoreInitialization :The score starts at zero:
score = 0
Score Incrementation:The Pygame's timer functionality is relied upon to create custom events. One such event, named scoreTimer, is set up to trigger every second (1000 milliseconds):
scoreTimer = pygame.event.custom_type()
pygame.time.set_timer(scoreTimer, 1000)
Inside the main game loop, whenever the scoreTimer event is detected, the score increments by one:
if(event.type == scoreTimer):
score += 1
Displaying the Score:The function display_score() is responsible for rendering the score on the screen. This function:
- Formats the score as a string, e.g., "Score: 5".
- Renders this string as a visual surface, using the specified font and color.
- Draws a rectangle around the score, adding a visual touch.
- Blits the score surface onto the main game screen at a specific location. Here's the key section of the display_score() function:
score_text = f'Score: {score}'
text_surf = font.render(score_text, True, (255, 255, 255))
text_rect = text_surf.get_rect(midbottom=(WINDOW_WIDTH / 2, WINDOW_HEIGHT - 80))
display_surface.blit(text_surf, text_rect)
gameMode VariableThe gameMode variable acts as a Boolean flag that determines the current state of the game. Specifically:
- When gameMode is set to False: The game is in its "standby" or "initial" state. This could mean that the game is at its start menu, paused, or in some other non-active gameplay state.
- When gameMode is set to True: The game is in its "active" state, and the primary gameplay mechanics are engaged. This is the state where the player is actively controlling the character, avoiding obstacles, and accruing points.
Throughout the code, this variable is checked to decide whether certain game functions or mechanics should be executed. For instance, cloud spawning, score accumulation, and game character motion are conditioned on gameMode being True. By using this flag, the game's logic can easily differentiate between active and passive game states, ensuring that certain mechanics only come into play during the actual gameplay.
Game Over Reset:If there is a collision detected between the DPSMan character and any of the clouds, the game mode variable is set to False (effectively ending the game). The code does not reset the score to zero immediately upon collision, the score is shown below the start menu, however by pressing s, the game restarts and resets the score.
Programming the DPSKits2GoThe DPSKit2Go is essentially a microcontroller evaluation board with a built-in barometric pressure sensor. (essentially an XMC2Go hooked up with a DPSShield2Go in one PCB) For more info on the DPSKits2Go click here!
As established in the setup subsection, we need our DPSKits to be constantly streaming their pressure readings to the PC.
Library: We're using the Dps310.h library. This library gives us all the functions we need to easily talk to the DPS310 sensor without getting into the nitty-gritty of sensor communication.
Setting up the Sensor:
#include <Dps310.h>
This includes the header file for the DPS310 library, which provides functions and definitions needed to communicate with the DPS310 sensor.
Dps310 Dps310PressureSensor = Dps310();
An instance (Dps310PressureSensor) of the Dps310 class is created. This object will be used to interact with the sensor.
void setup()
{
Serial.begin(9600);
while (!Serial);
Dps310PressureSensor.begin(Wire);
Serial.println("Init complete!");
}
- This is the setup function, which is called once when the Arduino starts.
- Serial.begin(9600); Initializes the serial communication with a baud rate of 9600.
- while (!Serial); Waits for the serial port to be ready.
- Dps310PressureSensor.begin(Wire); Initializes the Dps310PressureSensor object with the Wire library for I2C communication.
- Serial.println("Init complete!"); Sends a message to the serial monitor to indicate successful initialization.
void loop()
{
float temperature;
float pressure;
int pressureInt;
uint8_t oversampling = 7;
int16_t ret;
ret = Dps310PressureSensor.measurePressureOnce(pressure, oversampling);
Serial.println(pressure);
//Wait some time
delay(20);
}
void loop() {... }:
- This is the main loop that runs repeatedly after the setup function.
- float temperature; float pressure; Declares two float variables to hold temperature and pressure readings. Note that the temperature variable isn't actually used in this code.
- int pressureInt; Declares an integer variable pressureInt which isn't used in this code snippet either.
- uint8_t oversampling = 7; Sets the oversampling rate to 7. Oversampling can improve the precision of measurements by taking multiple readings and averaging them.
- int16_t ret; Declares a 16-bit signed integer to store the return value from the measurePressureOnce function.
- ret = Dps310PressureSensor.measurePressureOnce(pressure, oversampling); Measures the pressure once using the specified oversampling rate. The measured pressure is stored in the pressure variable.
- Serial.println(pressure); Sends the measured pressure value to the serial monitor.
- delay(20); Introduces a delay of 20 milliseconds before the loop starts again.
In the current stage of the project, wireless connectivity is definitely not essential. You could still run the same exact code while connecting the two DPS Kits by micro-USB to your computer of choice and everything will still run perfectly. (of course you'll have to change the COM Port numbers to suit your own) However in order to reduce the restriction from the USB cord I decided to use the HC 05 module, which is a Bluetooth based transceiver that acts as a serial COM Port.
Whenever powered on, the HC05 (if set to slave mode (default)), will go into advertising mode. In this mode, you could pair it with any device that has Bluetooth. Using your computer system of choice (as long as you have Bluetooth) you could pair with the module, which introduces two new COM ports to your system, one of which we will be using for our project. To find the one necessary you could flash the Arduino code provided on your DPSKit2Go that is connected to the HC05 module. Make sure that the serial output of the board is set to "On Board" instead of the computer. This will switch the UART Conversation from the pins that are involved in the USB Cable to the RX TX physical pins that are connected to the HC05 module. This also means that the serial monitor on your computer will not be able to read the values provided by the Kit if you connect it to your computer via USB Cable.
After that, you could use the Arduino serial monitor with each of the two ports we gained from pairing with the HC05 and check which one receives pressure readings.
CircuitThe HC05 module requires only 4 pins to be connected for operation. Actually, we could even connect only three. The UART TX and RX of the DPSKit gets connected to the RX and TX respectively of the HC05 module. (We could also just connect the TX of the DPSKit to the RX of the HC 05, since the DPSKit is not receiving any values.)
To power the Kit a small power bank was used, which was powering the kit (and in turn powering the HC05) through the micro-USB port on the DPSKit.
Comments
Please log in or sign up to comment.