-
Notifications
You must be signed in to change notification settings - Fork 93
Getting Started 1: Setting Up an App
This tutorial will teach you the basics of setting up a KivEnt game app. By the end this tutorial series you will be able to render an animated starfield background to the screen using a series of sprites taken from a sprite sheet.
Part One of this tutorial will use the following examples from the repository:
Prerequisites:
You will need to have compiled kivent_core
as well as kivy and all its requirements installed.
Additionally you will need the assets folder from the examples
directory.
We will start with an basic kivy application. In addition to the standard kivy items, we will import KivEnt (kivent_core).
Our main.py
should look as follows:
import kivy
from kivy.app import App
from kivy.uix.widget import Widget
import kivent_core
class TestGame(Widget):
pass
class YourAppNameApp(App):
def build(self):
pass
if __name__ == '__main__':
YourAppNameApp().run()
In addition to our python script, we will create the .kv file that corresponds to our simple kivy application. The corresponding .kv, yourappname.kv
, is shown below:
#:kivy 1.9.0
TestGame:
<TestGame>:
We will introduce 2 widgets, a GameWorld
which will manage your game data, and a GameScreenManager
which will allow us to sync our Kivy UI to the state of the GameWorld. Screens added to the GameScreenManager will work with GameWorld's state management.
<TestGame>:
gameworld: gameworld
GameWorld:
id: gameworld
gamescreenmanager: gamescreenmanager
size_of_gameworld: 100*1024
size_of_entity_block: 128
system_count: 8
zones: {'general': 10000}
GameScreenManager:
id: gamescreenmanager
size: root.size
pos: root.pos
gameworld: gameworld
GameScreen:
name: 'main'
We must configure a few options on the GameWorld. size_of_gameworld
is the number of Kibibytes that will be allocated to hold static component data (the non-python components that have been cythonized extensively). size_of_entity_block
is the size of the individual blocks (in kibibytes) of the memory pools for the GameWorld's entity memory. system_count
is the maximum number of GameSystem with components. The size of the entities memory will be sum of the count for all zones * system_count + 1. The zones
dict specifies which regions to reserve in memory and how large to make the regions. This allows you to organize your efficiencies by processing pattern so that our GameSystem can process a more contiguous arrangement of data. To learn more, read the memory handler documentation.
When creating a GameWorld, we need to first call init_gameworld
to finalize memory allocation and help us avoid kv creation races within our GameWorld widget tree. The first argument in this function is a list of the system_id
s for GameSystems we need to ensure have finished their initialization before commencing the init_gameworld
function. The callback
argument allows you to set a function to be called after the initialization has finished. We will make use of that callback here:
import kivy
from kivy.app import App
from kivy.uix.widget import Widget
import kivent_core
class TestGame(Widget):
def __init__(self, **kwargs):
super(TestGame, self).__init__(**kwargs)
self.gameworld.init_gameworld([], callback=self.init_game)
def init_game(self):
self.setup_states()
self.set_state()
def setup_states(self):
self.gameworld.add_state(state_name='main',
systems_added=[],
systems_removed=[], systems_paused=[],
systems_unpaused=[],
screenmanager_screen='main')
def set_state(self):
self.gameworld.state = 'main'
class YourAppNameApp(App):
def build(self):
pass
if __name__ == '__main__':
YourAppNameApp().run()
The add_state
function allows us to define states which will control which GameSystem have been added or removed from the widget tree, and are paused or unpaused, controlling the flow of your update loop. In addition you can specify the screen in the GameScreenManager to associate with this state. We must then set our GameWorld to one of the states we create to start the flow of our game.
On starting up your application you should receive a black screen (as we haven't added anything yet) and see some log output that looks like:
[INFO ] [KivEnt ] We will need 384 KiB for game, we have 102400 KiB
This is the total space we have reserved for our game, and the amount that we are actually using between all initialized GameSystems and the entity array itself. While not a count of the total memory your app will consume, as any python logic and the interpreter itself will take up a significant amount of memory as well, but it will help you reason about the size of the data your game logic is introducing and processing. This will ensure we do a minimum amount of memory allocation as a result of the game logic we will be introducing. Here we see only the amount of memory that will be required to hold 10,000 entities with 8 components each in the entities array.
We will start by adding a static renderer, and rendering some stars in the background. This will require adding 2 GameSystems: Renderer
and PositionSystem2D
. Let's change the TestGame
rule to:
<TestGame>:
gameworld: gameworld
GameWorld:
id: gameworld
gamescreenmanager: gamescreenmanager
size_of_gameworld: 100*1024
size_of_entity_block: 128
system_count: 2
size: root.size
pos: root.pos
zones: {'general': 10000}
PositionSystem2D:
system_id: 'position'
gameworld: gameworld
zones: ['general']
size_of_component_block: 128
Renderer:
id: renderer
gameworld: gameworld
system_id: 'renderer'
zones: ['general']
frame_count: 1
updateable: False
force_update: True
size_of_batches: 128
size_of_component_block: 128
shader_source: 'assets/glsl/positionshader.glsl'
GameScreenManager:
id: gamescreenmanager
size: root.size
pos: root.pos
gameworld: gameworld
GameScreen:
name: 'main'
When we declare the PositionSystem2D
, we give it a system_id
, which is the 'name' of its component, and a zones list, which tells the GameWorld how to allocate the memory for this GameSystem, the count for each zone name in the list will be reserved for this system, making the maximum number of entities that can be supported by this GameSystem the sum of the counts for each zone name.
We will make this renderer static as we are using it to draw immobile background sprites. This is accomplished by setting the frame_count
to 1 and updateable
to False. size_of_batches
controls the size of each batch submission to the GPU, Unity3d guys have recommended 1 Mib to 4 Mib sizes, here I have set it to 128 Kib. Finally we link our shader using shader_source
.
texture_manager.load_atlas('assets/background_objects.atlas')
First we load the atlas containing our textures, and then inside TestGame we load a model of the 'star1' texture with 2 different sizes (7., 7.) and (10., 10.).
class TestGame(Widget):
def __init__(self, **kwargs):
super(TestGame, self).__init__(**kwargs)
#Add the GameSystem we are initializing to the args in init_gameworld
self.gameworld.init_gameworld(
['renderer', 'position'],
callback=self.init_game)
def init_game(self):
self.setup_states()
self.set_state()
self.load_models()
def load_models(self):
model_manager = self.gameworld.model_manager
model_manager.load_textured_rectangle('vertex_format_4f', 7., 7.,
'star1', 'star1-4')
model_manager.load_textured_rectangle('vertex_format_4f', 10., 10.,
'star1', 'star1-4-2')
All of our sprites will share the same model unless we create a copy, as we do when creating the 'star1-4-2' model, this way you can modify some entities model without affecting all entities.
Finally, let us add a new function to our init_game
method that draws some things.
class TestGame(Widget):
def __init__(self, **kwargs):
super(TestGame, self).__init__(**kwargs)
#Add the GameSystem we are initializing to the args in init_gameworld
self.gameworld.init_gameworld(
['renderer', 'position'],
callback=self.init_game)
def init_game(self):
self.setup_states()
self.load_models()
self.set_state()
self.draw_some_stuff()
def draw_some_stuff(self):
init_entity = self.gameworld.init_entity
for x in range(1000):
pos = randint(0, Window.width), randint(0, Window.height)
model_key = choice(['star1-4', 'star1-4-2'])
create_dict = {
'position': pos,
'renderer': {'texture': 'star1',
'model_key': model_key},
}
#in addition to the args dict we pass in a list dictating
#the order to create the components in.
ent = init_entity(create_dict, ['position', 'renderer'])
#If you do not set Renderer.force_update to True, call update_trigger
#self.ids.renderer.update_trigger()
def setup_states(self):
#we add 'renderer' to ensure it is on the canvas and unpaused.
self.gameworld.add_state(state_name='main',
systems_added=['renderer'],
systems_removed=[], systems_paused=[],
systems_unpaused=['renderer'],
screenmanager_screen='main')
Now let us add a simple widget to our GameScreenManager to integrate a UI. It will display the current frame-per-second (FPS) performance of our application.
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)
In the kv file we will define:
<GameScreenManager>:
MainScreen:
id: main_screen
<MainScreen@GameScreen>:
name: 'main'
FloatLayout:
DebugPanel:
size_hint: (.2, .1)
pos_hint: {'x': .225, '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:'
Now if we run our application we should get some more output about KivEnt: Here we can see how much memory each of our systems will take, and some feedback about how many entities KivEnt estimates you will be able to render per batch.
[INFO ] [Kivent ] position allocated 1280 KiB
[INFO ] [KivEnt ] Batches for canvas: <kivy.graphics.instructions.RenderContext object at 0x7f80958f4650> will have 32768 verts and VBO will be 512 in KiB per frame with 20 total vbos, an estimated 8192 enities fit in each batch with 4 verts per entity
[INFO ] [Kivent ] renderer allocated 26260 KiB
[INFO ] [KivEnt ] We will need 28820 KiB for game, we have 102400 KiB
Let's draw more objects than we reserved, change the draw_some_stuff
function to draw 12,000 entities.
def draw_some_stuff(self):
init_entity = self.gameworld.init_entity
for x in range(12000):
We will get a traceback:
File "main.py", line 41, in draw_some_stuff
ent = init_entity(create_dict, ['position', 'renderer'])
File "kivent_core/gameworld.pyx", line 357, in kivent_core.gameworld.GameWorld.init_entity (kivent_core/gameworld.c:6724)
File "kivent_core/gameworld.pyx", line 338, in kivent_core.gameworld.GameWorld.get_entity (kivent_core/gameworld.c:6530)
File "kivent_core/managers/entity_manager.pyx", line 139, in kivent_core.managers.entity_manager.EntityManager.generate_entity (kivent_core/managers/entity_manager.c:2084)
File "kivent_core/memory_handlers/zone.pyx", line 271, in kivent_core.memory_handlers.zone.MemoryZone.get_free_slot (kivent_core/memory_handlers/zone.c:2527)
File "kivent_core/memory_handlers/pool.pyx", line 170, in kivent_core.memory_handlers.pool.MemoryPool.get_free_slot (kivent_core/memory_handlers/pool.c:1627)
MemoryError
If you are receiving a memory error you either need to increase the total memory available for your game or you need to raise the count for how many entities are in the zone you are creating your entity in.
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.clock import Clock
from kivy.core.window import Window
from random import randint, choice
import kivent_core
from kivent_core.gameworld import GameWorld
from kivent_core.systems.position_systems import PositionSystem2D
from kivent_core.systems.renderers import Renderer
from kivent_core.managers.resource_managers import texture_manager
from kivy.properties import StringProperty
texture_manager.load_atlas('assets/background_objects.atlas')
class TestGame(Widget):
def __init__(self, **kwargs):
super(TestGame, self).__init__(**kwargs)
self.gameworld.init_gameworld(
['renderer', 'position'],
callback=self.init_game)
def init_game(self):
self.setup_states()
self.load_models()
self.set_state()
self.draw_some_stuff()
def load_models(self):
model_manager = self.gameworld.model_manager
model_manager.load_textured_rectangle('vertex_format_4f', 7., 7.,
'star1', 'star1-4')
model_manager.load_textured_rectangle('vertex_format_4f', 10., 10.,
'star1', 'star1-4-2')
def draw_some_stuff(self):
init_entity = self.gameworld.init_entity
for x in range(5000):
pos = randint(0, Window.width), randint(0, Window.height)
model_key = choice(['star1-4', 'star1-4-2'])
create_dict = {
'position': pos,
'renderer': {'texture': 'star1',
'model_key': model_key},
}
ent = init_entity(create_dict, ['position', 'renderer'])
#If you do not set Renderer.force_update to True, call update_trigger
#self.ids.renderer.update_trigger()
def setup_states(self):
self.gameworld.add_state(state_name='main',
systems_added=['renderer'],
systems_removed=[], systems_paused=[],
systems_unpaused=['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):
def build(self):
Window.clearcolor = (0, 0, 0, 1.)
if __name__ == '__main__':
YourAppNameApp().run()
#:kivy 1.9.0
TestGame:
<TestGame>:
gameworld: gameworld
GameWorld:
id: gameworld
gamescreenmanager: gamescreenmanager
size_of_gameworld: 100*1024
size_of_entity_block: 128
system_count: 2
zones: {'general': 10000}
PositionSystem2D:
system_id: 'position'
gameworld: gameworld
zones: ['general']
size_of_component_block: 128
Renderer:
id: renderer
gameworld: gameworld
system_id: 'renderer'
zones: ['general']
frame_count: 1
updateable: False
force_update: True
size_of_batches: 512
size_of_component_block: 128
shader_source: 'assets/glsl/positionshader.glsl'
GameScreenManager:
id: gamescreenmanager
size: root.size
pos: root.pos
gameworld: gameworld
<GameScreenManager>:
MainScreen:
id: main_screen
<MainScreen@GameScreen>:
name: 'main'
FloatLayout:
DebugPanel:
size_hint: (.2, .1)
pos_hint: {'x': .225, '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 2: Creating a GameSystem