Why backend?
- Provide dynamic content
- Prevent cheating, especially in JavaScript based games
- Store sensitive data
- Collect data to provide game analytics
- Synchronization among devices
- Social component (leaderboards, communication between players)
- Multiplayer
Starting out
Prerequisites
- Python: - programming language.
- pip: - Python package manager.
- virtualenv: - tool to create isolated Python environments.
Steps
- Create a Python environment: mkvirtualenv game
- Install Django pip install Django==1.8.7
- Start Django project: django-admin.py startproject game
- cd game
- Apply initial migrations to the database: ./manage.py migrate
API
We will use Django REST framework - a powerful and flexible toolkit for building Web APIs. Let's install it:
$ pip install djangorestframework
And add to the INSTALLED_APPS in game/settings.py:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', ]
Suppose, that in our game each player has an inventory filled with items with weight. We would like to have a simple API to fetch inventory of a particular user.
First, let's create a new app called inventory: ./manage.py startapp inventory. Add inventory to INSTALLED_APPS as well.
The next step is to create Invenory and Item models, which will correspond to the tables in the database. Add the following code to inventory/models.py:
from django.db import models from django.contrib.auth.models import User from django.core.validators import MinValueValidator class Item(models.Model): name = models.CharField(max_length=255) weight = models.FloatField( validators=[MinValueValidator(0)], verbose_name='Weight in kg' ) def __str__(self): return self.name class Inventory(models.Model): user = models.OneToOneField(User) items = models.ManyToManyField(Item) class Meta: verbose_name_plural = 'inventories' def __str__(self): return "{}'s inventory".format(self.user.username)
Make and run migrations to add new tables to the database:
$ ./manage.py makemigrations $ ./manage.py migrate
Django comes with a handy admin dashboard. Let's our models to it - add these lines to inventory/admin.py:
from django.contrib import admin from .models import Item, Inventory admin.site.register([Item, Inventory])
Let's create a superuser:
$ ./manage.py createsuperuser
Now if we run Django server, we will be able to login to the admin dashboard located at localhost:8000/admin:
$ ./manage runserver
You can see Items and Inventories and you can add, change and delete records.
Now let's continue with our API. First, we define serializers for our models, which DRF will use to convert records to JSON. Then we add views, which will respond to API requests.
Create serializers.py and add:
from rest_framework import serializers from .models import Item, Inventory class ItemSerializer(serializers.ModelSerializer): class Meta: model = Item class InventorySerializer(serializers.ModelSerializer): class Meta: model = Inventory
Add to views.py:
from rest_framework import viewsets from .models import Item, Inventory from .serializers import ItemSerializer, InventorySerializer class ItemViewSet(viewsets.ModelViewSet): queryset = Item.objects.all().order_by('weight') serializer_class = ItemSerializer class InventoryViewSet(viewsets.ModelViewSet): queryset = Inventory.objects.all() serializer_class = InventorySerializer def get_queryset(self): """ Optionally restricts the returned purchases to a given user, by filtering against a `username` query parameter in the URL. """ queryset = Inventory.objects.all() username = self.request.query_params.get('username', None) if username is not None: queryset = queryset.filter(user__username=username) return queryset
Finally, let's specify urls at which our API will be accessible. Modify game/urls.py so it looks like this:
from django.conf.urls import url, include from django.contrib import admin from rest_framework import routers from inventory.views import ItemViewSet, InventoryViewSet router = routers.DefaultRouter() router.register(r'items', ItemViewSet) router.register(r'inventories', InventoryViewSet, base_name='inventories') urlpatterns = [ url(r'^api/', include(router.urls)), url(r'^admin/', admin.site.urls), ]
That's it! We can test our API:
$ curl -H 'Accept: application/json; indent=4' http://127.0.0.1:8000/api/items/ # Getting all items > [ { "id": 1, "name": "Guitar", "weight": 2.0 }, { "id": 2, "name": "Piano", "weight": 10.0 } ] $ curl -H 'Accept: application/json; indent=4' http://127.0.0.1:8000/api/inventories/?username=admin > [ { "id": 1, "user": 1, "items": [ 1, 2 ] } ]
Background and scheduled jobs
We will use Celery - an asynchronous task queue/job queue based on distributed message passing. Celery is used to schedule work either periodically or just not blocking the request thread. Typical examples are:
- Long running computations
- Periodic retrievel of data from external sources
Our example will be very simple - let's give each player a gift in a form of a new special item every 10 seconds.
Celery requires a message transport to send and receive messages. In our case it will be RabbitMQ, which should be installed on your machine.
Now let's install celery:
$ pip install celery
Create new file game/celery.py with the following content:
from __future__ import absolute_import import os from celery import Celery # set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game.settings') from django.conf import settings # noqa app = Celery('game') # Using a string here means the worker will not have to # pickle the object when using Windows. app.config_from_object('django.conf:settings') app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
The next thing is to create our task in inventory/tasks.py:
from django.contrib.auth.models import User from game.celery import app from .models import Item @app.task def give_gift(): for user in User.objects.iterator(): item, _ = Item.objects.create( name='Gift for {}'.format(user.username), weight=0) user.inventory.items.add(item)
Normally, you would execute this task by calling give_gift.delay(). That would put in the background. In our case, we want to use Celery beat, a scheduler, to make it a periodic task. All that's required is to add these lines to game/settings.py:
from datetime import timedelta CELERYBEAT_SCHEDULE = { 'give-gift-every-10-seconds': { 'task': 'inventory.tasks.give_gift', 'schedule': timedelta(seconds=10), }, }
Now we only have to start rabbitmq, celery worker and celery beat:
$ sudo rabbitmq-server $ celery -A game beat $ celery -A game worker
We can check results in Django shell (started by ./manage.py shell):
$ from django.contrib.auth.models import User $ u = User.objects.first() $ u.inventory.items.all() > [<Item: Guitar>, <Item: Piano>, <Item: Gift for admin>] $ u.inventory.items.all() # after 10 seconds > [<Item: Guitar>, <Item: Piano>, <Item: Gift for admin>, <Item: Gift for admin>]
Finite state machine
This is not really related to Django, but I would like to show how easily Django model can have a finit state machine with django-fsm package.
Let's create another app called enemies and add the following code to enemies/models.py:
from django.db import models from django.db.models import signals from django.dispatch import receiver from django.contrib.auth.models import User from django_fsm import FSMIntegerField, transition, can_proceed class Monster(models.Model): WANDERING = 10 ATTACKING = 20 LOOKING_FOR_AID = 30 EVADING = 40 STATES = ( (WANDERING, 'wandering'), (ATTACKING, 'attacking'), (LOOKING_FOR_AID, 'looking_for_aid'), (EVADING, 'evading') ) state = FSMIntegerField(default=WANDERING, choices=STATES) user_being_attacked = models.ForeignKey(User, default=None, null=True) health_points = models.IntegerField(default=10) def health_is_low(self): return self.health_points <= 5 @transition(field=state, source=[WANDERING], target=ATTACKING) def attack(self, user): self.user_being_attacked = user self.save() print('Attacking user') @transition(field=state, source=[ATTACKING, LOOKING_FOR_AID], target=WANDERING) def wander(self): self.user_being_attacked = None self.save() print('Just wandering') @transition(field=state, source=[ATTACKING], target=EVADING) def evade(self): pass @transition(field=state, source=[EVADING], target=LOOKING_FOR_AID, conditions=[health_is_low]) def look_for_aid(self): print('Looking for aid') @receiver(signals.post_save, sender=Monster) def post_save_callback(sender, **kwargs): monster = kwargs['instance'] if can_proceed(monster.look_for_aid): monster.look_for_aid() monster.save()
The FSM is taken from Finite-State Machines: Theory and Implementation:

As you can see, for each transition we can define source, target and conditions. We use Django's signals to listen when model is saved and try to look for aid. If conditions are not met or there is not transisiton, can_proceed will return False.
Let's test in the shell:
$ from enemies.models import Monster $ m = Monster.objects.create() $ m.get_state_display() > 'wandering' $ from django.contrib.auth.models import User $ u = User.objects.last() $ m.attack(u) > Attacking user $ m.get_state_display() > 'attacking' $ m.evade() $ m.get_state_display() > 'evading' $ m.health_points = 2 $ m.save() > Looking for aid
Real-time communication
Web frameworks as Django were not designed to handle real-time communication, put such functionality is more and more demanded. You can find several stable popular solution in the world of Python - Tornado, a web framework and asynchronous networking library, Twisted, an event-driven networking engine. Integration with Django can be problematic, but at least the code base can be shared.
Here we will take a look at one of the recent solution to this problem - SwampDragon, a real-time web framerowk built for Django with Tornado and Redis.
Install SwampDragon (pip install swampdragon), create a new app called players and add both players and swampdragon to INSTALLED_APPS. Ammend game/settings.py:
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': ['templates/'], ... } STATICFILES_DIRS = ['static/'] SWAMP_DRAGON_CONNECTION = ( 'swampdragon.connections.sockjs_connection.DjangoSubscriberConnection', '/data' ) DRAGON_URL = 'http://localhost:9999/'
Let's create a new model with SelfPublishModel mixin:
from django.db import models from swampdragon.models import SelfPublishModel from django.contrib.auth.models import User from .serializers import PlayerSerializer class Player(SelfPublishModel, models.Model): serializer_class = PlayerSerializer user = models.ForeignKey(User) position_x = models.FloatField() position_y = models.FloatField()
And a serializer in players/serializers.py (this is slightly inconvinient, since DRF also requires us to define serializers, but the work of integrating serializers from DRF is in progerss):
from swampdragon.serializers.model_serializer import ModelSerializer class PlayerSerializer(ModelSerializer): class Meta: model = 'players.Player' publish_fields = ('position_x', 'position_y') update_fields = ('position_x', 'position_y')
Instead of views, SwampDragon uses routers:
from swampdragon import route_handler from swampdragon.route_handler import ModelRouter from .models import Player from .serializers import PlayerSerializer class PlayerRouter(ModelRouter): route_name = 'player' serializer_class = PlayerSerializer model = Player def get_object(self, **kwargs): return self.model.objects.get(pk=kwargs['id']) def get_query_set(self, **kwargs): return self.model.objects.all() route_handler.register(PlayerRouter)
Now let's add templates. Create templates directory and index.html inside with the following content:
{% load static swampdragon_tags %} <!DOCTYPE html> <html ng-app="GameApp"> <head lang="en"> <meta charset="UTF-8"> <title></title> </head> <body> {% verbatim %} <div ng-controller="PlayerCtrl"> <ul> <li ng-repeat="player in players"> {{ player.position_x }} {{ player.position_y }} <button type="button" ng-click="moveRight(player)"> Move right </button> <button type="button" ng-click="moveLeft(player)"> Move Left </button> <button type="button" ng-click="moveUp(player)"> Move Up </button> <button type="button" ng-click="moveDown(player)"> Move Down </button> </li> </ul> </div> {% endverbatim %} <!-- AngularJS --> <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.21/angular.min.js"></script> <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.21/angular-route.js"></script> {% swampdragon_settings %} <script type="text/javascript" src="{% static 'swampdragon/js/dist/swampdragon.min.js' %}"></script> <script type="text/javascript" src="{% static 'swampdragon/js/dist/datamapper.js' %}"></script> <script type="text/javascript" src="{% static 'swampdragon/js/angular/services.js' %}"></script> <script type="text/javascript" src="{% static 'app.js' %}"></script> <script type="text/javascript" src="{% static 'controllers.js' %}"></script> </body>
Add a view, that will render this template to game/urls.py:
from django.conf.urls import url, include from django.contrib import admin from django.views.generic import TemplateView from rest_framework import routers from inventory.views import ItemViewSet, InventoryViewSet router = routers.DefaultRouter() router.register(r'items', ItemViewSet) router.register(r'inventories', InventoryViewSet) urlpatterns = [ url(r'^$', TemplateView.as_view(template_name='index.html'), name='home'), url(r'^api/', include(router.urls)), url(r'^admin/', admin.site.urls), ]
Create static directory for JavaScript files. First, let's define our app in app.js:
var GameApp = angular.module('GameApp', [ 'SwampDragonServices', 'PlayerControllers' ]);
and our controller in controllers.js:
var PlayerControllers = angular.module('PlayerControllers', []); PlayerControllers.controller('PlayerCtrl', ['$scope', '$dragon', function ($scope, $dragon) { $scope.players = []; $scope.channel = 'players'; $dragon.onReady(function() { $dragon.subscribe('player', $scope.channel).then(function(response) { $scope.dataMapper = new DataMapper(response.data); }); $dragon.getList('player').then(function(response) { $scope.players = response.data; }); }); $dragon.onChannelMessage(function(channels, message) { if (indexOf.call(channels, $scope.channel) > -1) { $scope.$apply(function() { $scope.dataMapper.mapData($scope.players, message); }); } }); var move = function(player, dx, dy) { player.position_x += dx; player.position_y += dy; $dragon.update('player', player); }; $scope.moveRight = function(player) { move(player, 1, 0); }; $scope.moveLeft = function(player) { move(player, 1, 0); }; $scope.moveUp = function(player) { move(player, 0, 1); }; $scope.moveDown = function(player) { move(player, 0, -1); }; }]);
To make real-time communication work we need an additional server running. Download server.py from SwampDragon repository and save it next to manage.py.
Now if you run both ./manage.py runserver and python server.py you can test our app in the browser.