Skip to main content

Game of life Perspective

This tutorial will be separated in a four different parts :

  1. The Grid Layout
  2. The Controller
  3. Animations are fun
  4. Dynamic Graphs are easy

  5. The first two will be about the base we need to apply the advanced notions.

 

Please keep in mind this has not been thought out for performance, but just as a simple tutorial to develop a perspective.

 

This plug-in of the week will span over a few weeks, and we will develop a simple game of life perspective in Tulip.


A perspective is a complete user interface that can be added to Tulip's default look and feel.

This is pretty similar to Eclipse perspectives (Code, Debug, …), in that you can make pretty much what you want (e.g. a social network analysis perspective) and still have all the power of Tulip.

 

I will not explain what Conway's Game of Life is, Wikipedia does it better.

 

So what we will need is a grid of nodes, each node connected to all of its neighbors (3 for the corner nodes; 5 for nodes on the sides and 8 for all the other nodes) so we can use Tulip to find the neighbors and not look at arrays (this will not be as efficient, be we don't care about this, remember ?).

 

We will have to layout the nodes in a grid, which will be the first task we tackle.

 

Then we will have to allow the user to make the game compute the next step, with, for instance, a simple button.

And of course display the graph, with “live” nodes and “dead” nodes, each represented by a different color.

 

  1. The grid layout

This is pretty straightforward, we just need to create a LayoutAlgorithm, and have it layout the nodes in a grid in its run function.


A layout algorithm has a property called layoutResult, which is the LayoutProperty it should modify in order to have the nodes and edges take the desired positions.

 

A minimal grid layout algorithm is described below:

  double nbNodes = graph->numberOfNodes();
  int numberOfColumns = ceil(sqrt(nbNodes));
  
  int currentColumn = 0;
  int currentRow = 0;
  Iterator<node>* it = graph->getNodes();
  while(it->hasNext()) {
    node n = it->next();
    
    layoutResult->setNodeValue(n, Coord(currentColumn, currentRow, 0));
    
    ++currentColumn;
    if(currentColumn >= numberOfColumns) {
      currentColumn = currentColumn % numberOfColumns;
      --currentRow;
    }
  }
  
  delete it;

 

 Once this is encapsulated in a Tulip Algorithm plug-in our firsty step is done !

We can use this algorithm in Tulip, and see how our grid will look like.

 

Next week, the interesting part:

The Controller

Creating a controller is as easy as creating any other plug-in, you just inherit from the right class, and register your plug-in into Tulip's system.

In the case of a Controller, the base class is ControllerViewsManager, and they are registered with the CONTROLLERPLUGIN macro.

 

The virtual functions we will override are setData, getData, and attachMainWindow.

    virtual void attachMainWindow(tlp::MainWindowFacade facade);
    virtual void getData(tlp::Graph** graph, tlp::DataSet* data);
    virtual void setData(tlp::Graph* = 0, tlp::DataSet = tlp::DataSet());

 

The first one is called when a graph is loaded (new graph, or graph opened), the second is used to save the graph, and the third is a handle to build the ui when the controller is created.

 

GetData only needs to return the graph, which is easy.

void GameOfLifePerspective::getData(tlp::Graph** graph, tlp::DataSet* set) {
  *graph = _graph;
  *set = _set;
}

For now, we will make things simple and create a new graph when setData is called. This should never be done, but in the first step of iterative development, we will do it anyways :)

void GameOfLifePerspective::setData(tlp::Graph*, tlp::DataSet) {
  _graph = tlp::newGraph();

 

We will create a simple grid of nodes and link them together (for now, we will not make it a torus but a simple grid).

int x = 0;
  int y = 0;
  for(int i = 0; i < 100; ++i) {
    nodes[x++][y] = _graph->addNode();
    if(x > 9) {
      x = x%10;
      ++y;
    }
  }
  //we want to link each node to its neighbors
  for(int x = 0; x < 10; ++x) {
    for(int y = 0; y < 10; ++y) {
      //for each node
      
      for(int a = (x > 0 ? -1 : 0); a <= (x < 9 ? 1 : 0); ++a) {
        for(int b = (y > 0 ? -1 : 0); b <= (y < 9 ? 1 : 0); ++b) {
	    if(!_graph->existEdge(nodes[x][y], nodes[x+a][y+b], false).isValid() && a != 0 || b != 0 ) {
//             std::cout << "linking (" << x << ";" << y <<") to (" << x+a << ";" << y+b << ")"  << std::endl; 
            _graph->addEdge(nodes[x][y], nodes[x+a][y+b]);
          }
        }
      }
    }
  }
 LayoutProperty* layout = _graph->getProperty<LayoutProperty>("viewLayout");
  string message = "";
  string algo = "Grid";
  _graph->computeProperty<LayoutProperty>(algo, layout, message);

 

Then set the color, border color, border width, and shape of the nodes.

ColorProperty* color = _graph->getProperty<ColorProperty>("viewColor");
  color->setAllNodeValue(Color(255, 0, 0, 100));
  ColorProperty* borderColor = _graph->getProperty<ColorProperty>("viewBorderColor");
  borderColor->setAllNodeValue(Color(0, 0, 0, 255));
  DoubleProperty* borderWidth = _graph->getProperty<DoubleProperty>("viewBorderWidth");
  borderWidth->setAllNodeValue(1);
  IntegerProperty* shape = _graph->getProperty<IntegerProperty>("viewShape");
  shape->setAllNodeValue(4); //4 is the magic number for square glyph

 

We will use two boolean properties to keep in memory whether a node is “alive” or not, and whether it will be alive in the next iteration or not.

isAlive = _graph->getProperty<tlp::BooleanProperty>("isAlive");
  willBeAlive = _graph->getProperty<tlp::BooleanProperty>("willBeAlive");
  willBeAlive->setAllNodeValue(false);
  isAlive->setAllNodeValue(false);

We will observe the property describing whether a node is alive or not, and when changes occur we will update the color of the node(s) accordingly.

isAlive->addPropertyObserver(this);

 

Well, this is pretty straightforward, isn't it ?

 

Now we need to add a Tulip view to our controller to see this.

AttachMainWindow is the place for this, and all we need to do is create a NodeLinkDiagramComponent, that will display the graph for us.

void GameOfLifePerspective::attachMainWindow(tlp::MainWindowFacade facade) {
  tlp::ControllerViewsManager::attachMainWindow(facade);
  
  QRect rect = QRect(0, 0, 0, 0);
  _display = dynamic_cast<NodeLinkDiagramComponent*>(createView("Node Link Diagram view", _graph, tlp::DataSet(), false, rect, true));
  _display->hideOverview(true);

 

Now, we need to interact with this graph, to change the state of nodes from alive to dead , and vice-versa.

There is already an interactor tutorial, so I will not delve too deep into this, as it is extremely straightforward: when there is a mouse click, we check if there is a node under the mouse, and if so, we change the state of the property isAlive.

 

We set this interactor to be the only interactor available, as we do not want to allow the user to add or remove nodes, nor move them. We could easily set a list of the interactors we allow, but this will be for a later tutorial.

list<Interactor*> interactors;
  Interactor* cellStateChanger = new TurnCellOnOffInteractor(":/i_addnode.png", "Switch the state of a cell");
  interactors.push_back(cellStateChanger);
  _display->setInteractors(interactors);
  installInteractors(_display);
  _display->setActiveInteractor(cellStateChanger);

 

All we need now is to play the game.

For this we need to compute the next state of the game, which I will not detail either, and to allow the user to launch said computation.

I added a QDockWidget in the bottom of the screen containing a button, linked by a Qt Signal to the controller's nextStep() function.

QDockWidget* controlsDock = new QDockWidget("controls");
  controlsDock->setFeatures(QDockWidget::NoDockWidgetFeatures);
  
  QPushButton* OneStepForward = new QPushButton(QApplication::style()->standardIcon(QStyle::SP_MediaSeekForward), "Move one step forward in time");
  connect(OneStepForward, SIGNAL(clicked(bool)), this, SLOT(nextStep()));
  
  controlsDock->setWidget(OneStepForward);
  
  mainWindowFacade.addDockWidget(Qt::BottomDockWidgetArea, controlsDock);

 

Well, now you know how to create a controller, and you can do pretty much anything you want with it: change the menus, toolbars, define a single view to be displayed, or any number as in the Tulip default perspective.

You can add controls, and compute anything you want.

 

Next step will see a bit of fun: we will animate the transition from alive to dead, using a tulip framework to compute interpolations between property values.