Adventures in Symfony – Using Open Source Software to Make an MMO Game – Margaret Staples



Adventures in Symfony – Using Open Source Software to Make an MMO Game – Margaret Staples

0 0


GameDev


On Github mstaples / GameDev

Adventures in Symfony

Using Open Source Software to Make an MMO Game

Margaret Staples

Hello, nice to meet you!

I'm Margaret Staples

Hello, nice to meet you!

I'm Margaret Staples

I've been developing a Social Strategy City-Builder RPG

game of awesomeness.

Overview

Planning

Core Systems Development

Player Testing

Feature Completion

Deployment

Planning

Planning

Inventory Design Info

Inventory Design Info

Locate, Categorize, Analyze.

Planning

Conceptualize Development

Conceptualize Development

“No Battle Plan Survives Contact With the Enemy”

Planning

Initial Database Design

Initial Database Design

Resources

Goods

Buildings

Items

Actions

Districts

Classes

Attributes

Units

Planning

Learn the Framework

"Learn" the "Framework"

Core Systems

harvesting, processing, crafting, training, building, actions, leveling

Core Systems

Initial Database Content

Initial Database Content

Install Doctrine Fixtures Bundle

Create Entities

> composer require doctrine/doctrine-fixtures-bundle
 // src/Group/NameBundle/Entity/Resources.php
/**
 * @ORM\Entity
 * @ORM\Table(name="resources")
 */
> php app/console doctrine:schema:update --force
> php app/console doctrine:generate:entities Group/NameBundle/Entity/Resources

Core Systems

Initial Database Content

Initial Database Content

Fixtures and Generated Data

Google Docs

Design Documents

Fixture Spreadsheets

Export to CSV

// [ProjectDirectory]/src/[Group]/[Bundle]/Datafixtures/ORM/exports

Core Systems

Initial Database Content

Initial Database Content

// src/Group/NameBundle/DataFixtures/ORM/LoadResourcesData.php

namespace Group\NameBundle\DataFixtures\ORM;

use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Group\NameBundle\Entity\Resources;

class LoadResourcesData implements FixtureInterface
{
    public function load(ObjectManager $manager)
    {
        $filename = __DIR__.'/exports/resources.txt';
        $fd = fopen($filename, "r");
        $contents = fread($fd, 10000);
        fclose($fd);
        $splitcontents = explode(',', $contents);
        $counter=0;
        foreach ($splitcontents as $each) {
            if ($counter == 0) {
                $resource = new Resources();
                $resource->setName($each);
            }
            ...
        }
    }
}
> php app/console doctrine:fixtures:load

Core Systems

Initial Database Content

Initial Database Content

// src/Group/NameBundle/Command/startOneCommand.php

namespace Group\NameBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Output\OutputInterface;
use ONN\BrunBundle\Entity\Resources;

class startOneCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this->setName('start:one')
             ->setDescription('setup the game server');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $em = $this->getContainer()->get('doctrine')->getManager('default');

        $output->writeln('step one process: generating Wilderness');
        $this->generateWilderness();
        $output->writeln('... complete');
    }
}
> php app/console start:one

Core Systems

Initial Interactions

Initial Interactions

Harvest, Process, Construction

Core Systems

Initial Time Based Events

Initial Time Based Events

Harvest, Process, Construction

Timed Event Tables:

  • EventMin
  • EventHour
  • EventDay
  • EventWeek

Core Systems

Initial Time Based Events

Initial Time Based Events

Harvest, Process, Construction

// src/Group/NameBundle/Command/timeCommand.php
...
class timeCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            ->setName('time')
            ->setDescription('Triggers timed event processing')
            ->setDefinition(array(
                new InputArgument(
                    'timer',
                    InputArgument::REQUIRED,
                    'timer name'
                ),
            ));
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $timer = $input->getArgument('timer');
    }
}
> php app/console time days

Core Systems

Initial Time Based Events

Initial Time Based Events

Harvest, Process, Construction

#!/bin/bash
# scripts/brunday.bsh

logfile=~/logs/bruncycle.log
exec >> $logfile 2>&1
export PATH
echo "Brun Daily Cycle:" >> ~/logs/bruncycle.log
php ~/www/app/console time days 
> crontab -e

0    0    1    *    *    ~/scripts/brunback.bsh
01   *    *    *    *    ~/scripts/brunday.bsh
01   01   *    *    *    ~/scripts/brunweek.bsh
*/15 *    *    *    *    ~/scripts/brunhour.bsh
*/5  *    *    *    *    ~/scripts/checkcycle.bsh

Core Systems

Initial UI

Initial UI

Like a MUD with web forms

public function someFormAction(Request $request, $active = 'no') 
{
    $form_name = "someForm";
    $form_path = "some_form";
    
    $form = $this->createFormBuilder(null)
        ...
        ->getForm();

    return $this->render('GroupNameBundle:None:formFragment.html.twig', [
        'form' => $form->createView(), 
        'form_name'=>$form_name,
        'form_path'=>$form_path
    ));
}

Core Systems

Initial UI

Initial UI

Like a MUD with web forms

{# src/Group/NameBundle/Resources/views/None/formFragment.html.twig #}

<script>
    function {{ form_name }}Submit() {
        $.post( '{{ path( form_path, { 'active': 'yes' }) }}',
            $('#form_{{ form_name }}').serialize(),
            function(data){
                $('#container_{{ form_name }}').empty().append(data);
            }
        );
        return false;
    }
</script>

<div id="container_{{ form_name }}">
    <form id='form_{{ form_name }}' method="post" 
        {{ form_enctype(form) }} name="{{ form_name }}" >
        
        {{ form_widget(form) }}
        
        <input id="{{ form_name }}" onclick="{{ form_name }}Submit()" 
            value='Submit' class='btn btn-small btn-custom' />
    </form>
</div>

Core Systems

First Exposure to "Community"

First Exposure to "Community"

Core Systems

Expanded Interactions

Expanded Interactions

Crafting, Training, Actions, Leveling

Player Testing

dissolution of assumptions

Player Testing

Players Do Things Differently

Players . . .

Do Things Differently

Create Unpredicted Strain

Provide Design Clarity

Player Testing

Communicating with Players

Communicating with Players

Player Testing

Prioritizing Reports and Requests

Prioritizing Reports and Requests

Feature Completion

Feature Completion

Organization Concerns

Organization Concerns

What Goes Where, With Access to What

Controllers

Services

Repositories

Commands

Feature Completion

Peripheral Features

"Peripheral" Features

Require as Much or More as "Core" Features

JIT Design Completion:

Less Flexible, Harder Refactors

Feature Completion

Deliberate Testing

Deliberate Testing

Feature Completion

Time Based Events: Redux

Time Based Events: Redux

RabbitMQ

> composer require oldsound/rabbitmq:1.*
# app/config/config.yml
old_sound_rabbit_mq:
    connections:
        default:
            host:     'localhost'
            port:     5672
            user:     %rabbit_user%
            password: %rabbit_password%
            ...
    producers:
        queue_task:
            ...
    consumers:
        queue_task:
            ...

Feature Completion

Time Based Events: Redux

Time Based Events: Redux

RabbitMQ

# src/Group/NameBundle/Consumer/ProcessQueueConsumer.php
...
class ProcessQueueConsumer implements ConsumerInterface
{
    public function __construct(EntityManager $entityManager, $kernel)
    {
        ...
    }

    public function execute(AMQPMessage $msg)
    {
        $kernel = $this->kernel;
        $command = $msg->body;
        ...
        $path = str_replace('scripts','','php '. $kernel->getRootDir() . '/console ' . $command);
        $process = new Process($path);
        $process->start();
        ...
        return $process->getOutput();
    }
}
$command = 'update-act-access '.$char_id;
$this->get('old_sound_rabbit_mq.queue_task_producer')->publish($command);

Feature Completion

Time Based Events: Redux

Time Based Events: Redux

Services, Commands, and RabbitMQ

#!/bin/bash
# scripts/checkworkers.bsh

NB_TASKS=15
SYMFONY_ENV="prod"

TEXT[0]="app/console rabbitmq:consumer -l 256 queue_task"

for text in "${TEXT[@]}"
do

NB_LAUNCHED=$(ps ax | grep "$text" | grep -v grep | wc -l)

TASK="/usr/bin/env php ${text} --env=${SYMFONY_ENV}"

for (( i=${NB_LAUNCHED}; i<${NB_TASKS}; i++ ))
do
  echo "$(date +%c) - Launching a new consumer"
  nohup $TASK > consumer_$i.log &
done

done

Feature Completion

Reflecting Change for Active Players

Reflecting Change for Active Players

Feature Completion

API

API

Feature Completion

3rd Party Code Upgrades

3rd Party Code Upgrades

Some More Painful Than Others

Deployment

Feature Completion

Estimates are Hard

Estimates are Hard

Feature Completion

3rd Party Code Upgrades

Magic and Dreams

Adventures in Symfony

Using Open Source Software to Make an MMO Game

brunegame.com

Questions?

brunegame.com

Adventures in Symfony

Using Open Source Software to Make an MMO Game

brunegame.com

Margaret Staples

twitter: @dead_lugosi

freenode: deadlugosi

email: mstaples@aesopgames.com

joind.in/14930

Adventures in Symfony Using Open Source Software to Make an MMO Game Margaret Staples