Skip to content

Getting Started 3: Adding the Physics System

SleepProgger edited this page Apr 24, 2017 · 7 revisions

The CymunkPhysics System

In this tutorial we will add physics objects to our example application from Getting Started: 1.

The content of this tutorial covers the directories:

  • examples/4_adding_physics_objects

Prerequisites: You will need to have compiled kivent_core, kivent_cymunk, and cymunk, in addition to having kivy and all its requirements installed.

Setting Up The Dependent Systems

Currently we have a renderer that is only good for position data. Our physics objects will rotate so we need to use a renderer with a different vertex shader, and an appropriate shader. We will add 3 GameSystems: RotateSystem2D, CymunkPhysics, and RotateRenderer (replacing the old renderer). Note that the new renderer uses the 'positionrotateshader.glsl' instead of 'positionshader.glsl' we used in the last few examples.

<TestGame>:
    gameworld: gameworld
    app: app
    GameWorld:
        id: gameworld
        gamescreenmanager: gamescreenmanager
        size_of_gameworld: 100*1024
        zones: {'general': 20000}
        PositionSystem2D:
            system_id: 'position'
            gameworld: gameworld
            zones: ['general']
        RotateSystem2D:
            system_id: 'rotate'
            gameworld: gameworld
            zones: ['general']
        RotateRenderer:
            gameworld: gameworld
            zones: ['general']
            shader_source: 'assets/glsl/positionrotateshader.glsl'
        CymunkPhysics:
            gameworld: root.gameworld
            zones: ['general']
    GameScreenManager:
        id: gamescreenmanager
        size: root.size
        pos: root.pos
        gameworld: gameworld

Initializing a Physics Entity

Let's modify our create entity function to look like this:

    def create_asteroid(self, pos):
        x_vel = randint(-500, 500)
        y_vel = randint(-500, 500)
        angle = radians(randint(-360, 360))
        angular_velocity = radians(randint(-150, -150))
        shape_dict = {'inner_radius': 0, 'outer_radius': 20, 
            'mass': 50, 'offset': (0, 0)}
        col_shape = {'shape_type': 'circle', 'elasticity': .5, 
            'collision_type': 1, 'shape_info': shape_dict, 'friction': 1.0}
        col_shapes = [col_shape]
        physics_component = {'main_shape': 'circle', 
            'velocity': (x_vel, y_vel), 
            'position': pos, 'angle': angle, 
            'angular_velocity': angular_velocity, 
            'vel_limit': 250, 
            'ang_vel_limit': radians(200), 
            'mass': 50, 'col_shapes': col_shapes}
        create_component_dict = {'cymunk_physics': physics_component, 
            'rotate_renderer': {'texture': 'asteroid1', 
            'size': (45, 45),
            'render': True}, 
            'position': pos, 'rotate': 0, }
        component_order = ['position', 'rotate', 'rotate_renderer', 
            'cymunk_physics',]
        return self.gameworld.init_entity(
            create_component_dict, component_order)

A physics component is fairly complicated, and you'd be best of reading Chipmunk2D's documentation to understand every parameter here, however the basics are that cymunk support several types of shapes: 'circle', 'box', 'poly', and 'segment' that correspond to various types of geometric objects Chipmunk2D can calculate the physics between. Each shape has different configuration options: you can find the expected values here.

In addition, every shape shares many other physics related values such as mass, position, velocity, angle, and so on. We add all these arguments for initializing our physics object and we add a rotate component to receive the physics information.

Adding Entities on User Input

Let's set up a button to draw our entities instead of drawing them in the init_game method. Remove the draw_some_stuff function from our init_game function:

    def init_game(self):
        self.setup_states()
        self.set_state()

and instead in our KV UI, let's add:

<MainScreen@GameScreen>:
    name: 'main'
    FloatLayout:
        Button:
            text: 'Draw Some Stuff'
            size_hint: (.2, .1)
            pos_hint: {'x': .025, 'y': .025}
            on_release: app.root.draw_some_stuff()
        DebugPanel:
            size_hint: (.2, .1)
            pos_hint: {'x': .225, 'y': .025}
        Label:
            text: str(app.count)
            size_hint: (.2, .1)
            font_size: 24
            pos_hint: {'x': .425, 'y': .025}

We'll also add a count NumericProperty to our App class and a label to display the count:

class YourAppNameApp(App):
    count = NumericProperty(0)

The new draw_some_stuff function looks like:

    def draw_some_stuff(self):
        size = Window.size
        w, h = size[0], size[1]
        delete_time = 2.5
        create_asteroid = self.create_asteroid
        for x in range(100):
            pos = (randint(0, w), randint(0, h))
            ent_id = create_asteroid(pos)
        self.app.count += 100

Removing Entities

Finally, we will remove the entities a certain amount of time after we add them using a kivy Clock.schedule_once.

Add a callback function to clockschedule:

    def destroy_created_entity(self, ent_id, dt):
        self.gameworld.remove_entity(ent_id)
        self.app.count -= 1

Modify the draw_some_stuff function to call this function using the entity_id returned by our create_asteroid function:

    def draw_some_stuff(self):
        size = Window.size
        w, h = size[0], size[1]
        delete_time = 2.5
        create_asteroid = self.create_asteroid
        destroy_ent = self.destroy_created_entity
        for x in range(100):
            pos = (randint(0, w), randint(0, h))
            ent_id = create_asteroid(pos)
            Clock.schedule_once(partial(destroy_ent, ent_id), delete_time)
        self.app.count += 100

We use partial to create a function that has the entity_id as an arg and will receive the appropriate dt term from the Kivy Clock.

Full Code

Make sure to update the state for your application to use the new GameSystem.

from kivy.app import App
print('imported kivy')
from kivy.uix.widget import Widget
from kivy.clock import Clock
from kivy.core.window import Window
from random import randint, choice
from math import radians, pi, sin, cos
import kivent_core
import kivent_cymunk
from kivent_core.gameworld import GameWorld
from kivent_core.managers.resource_managers import texture_manager
from kivent_core.systems.renderers import RotateRenderer
from kivent_core.systems.position_systems import PositionSystem2D
from kivent_core.systems.rotate_systems import RotateSystem2D
from kivy.properties import StringProperty, NumericProperty
from functools import partial


texture_manager.load_atlas('assets/background_objects.atlas')


class TestGame(Widget):
    def __init__(self, **kwargs):
        super(TestGame, self).__init__(**kwargs)
        self.gameworld.init_gameworld(
            ['cymunk_physics', 'rotate_renderer', 'rotate', 'position',],
            callback=self.init_game)

    def init_game(self):
        self.setup_states()
        self.set_state()

    def destroy_created_entity(self, ent_id, dt):
        self.gameworld.remove_entity(ent_id)
        self.app.count -= 1

    def draw_some_stuff(self):
        size = Window.size
        w, h = size[0], size[1]
        delete_time = 2.5
        create_asteroid = self.create_asteroid
        destroy_ent = self.destroy_created_entity
        for x in range(100):
            pos = (randint(0, w), randint(0, h))
            ent_id = create_asteroid(pos)
            Clock.schedule_once(partial(destroy_ent, ent_id), delete_time)
        self.app.count += 100

    def create_asteroid(self, pos):
        x_vel = randint(-500, 500)
        y_vel = randint(-500, 500)
        angle = radians(randint(-360, 360))
        angular_velocity = radians(randint(-150, -150))
        shape_dict = {'inner_radius': 0, 'outer_radius': 20, 
            'mass': 50, 'offset': (0, 0)}
        col_shape = {'shape_type': 'circle', 'elasticity': .5, 
            'collision_type': 1, 'shape_info': shape_dict, 'friction': 1.0}
        col_shapes = [col_shape]
        physics_component = {'main_shape': 'circle', 
            'velocity': (x_vel, y_vel), 
            'position': pos, 'angle': angle, 
            'angular_velocity': angular_velocity, 
            'vel_limit': 250, 
            'ang_vel_limit': radians(200), 
            'mass': 50, 'col_shapes': col_shapes}
        create_component_dict = {'cymunk_physics': physics_component, 
            'rotate_renderer': {'texture': 'asteroid1', 
            'size': (45, 45),
            'render': True}, 
            'position': pos, 'rotate': 0, }
        component_order = ['position', 'rotate', 'rotate_renderer', 
            'cymunk_physics',]
        return self.gameworld.init_entity(
            create_component_dict, component_order)

    def update(self, dt):
        self.gameworld.update(dt)

    def setup_states(self):
        self.gameworld.add_state(state_name='main', 
            systems_added=['rotate_renderer'],
            systems_removed=[], systems_paused=[],
            systems_unpaused=['rotate_renderer'],
            screenmanager_screen='main')

    def set_state(self):
        self.gameworld.state = 'main'


class DebugPanel(Widget):
    fps = StringProperty(None)

    def __init__(self, **kwargs):
        super(DebugPanel, self).__init__(**kwargs)
        Clock.schedule_once(self.update_fps)

    def update_fps(self,dt):
        self.fps = str(int(Clock.get_fps()))
        Clock.schedule_once(self.update_fps, .05)

class YourAppNameApp(App):
    count = NumericProperty(0)


if __name__ == '__main__':
    YourAppNameApp().run()
#:kivy 1.9.0

TestGame:

<TestGame>:
    gameworld: gameworld
    app: app
    GameWorld:
        id: gameworld
        gamescreenmanager: gamescreenmanager
        size_of_gameworld: 100*1024
        zones: {'general': 20000}
        PositionSystem2D:
            system_id: 'position'
            gameworld: gameworld
            zones: ['general']
        RotateSystem2D:
            system_id: 'rotate'
            gameworld: gameworld
            zones: ['general']
        RotateRenderer:
            gameworld: gameworld
            zones: ['general']
            shader_source: 'assets/glsl/positionrotateshader.glsl'
        CymunkPhysics:
            gameworld: root.gameworld
            zones: ['general']
    GameScreenManager:
        id: gamescreenmanager
        size: root.size
        pos: root.pos
        gameworld: gameworld

<GameScreenManager>:
    MainScreen:
        id: main_screen

<MainScreen@GameScreen>:
    name: 'main'
    FloatLayout:
        Button:
            text: 'Draw Some Stuff'
            size_hint: (.2, .1)
            pos_hint: {'x': .025, 'y': .025}
            on_release: app.root.draw_some_stuff()
        DebugPanel:
            size_hint: (.2, .1)
            pos_hint: {'x': .225, 'y': .025}
        Label:
            text: str(app.count)
            size_hint: (.2, .1)
            font_size: 24
            pos_hint: {'x': .425, 'y': .025}

<DebugPanel>:
    Label:
        pos: root.pos
        size: root.size
        font_size: root.size[1]*.5
        halign: 'center'
        valign: 'middle'
        color: (1,1,1,1)
        text: 'FPS: ' + root.fps if root.fps != None else 'FPS:'

Continue to Getting Started 4: Interacting with the Physics System