AR Board Game
This blog post explains certain components that are required to build an AR Board Game in Flutter for the iOS platform.
Why Flutter
Flutter has become one of the most popular choices for mobile app development that can support both iOS and Android. The most apparent advantage is having to maintain a single codebase that can be compiled to both platforms.
This tutorial will only focus on building iOS AR app using flutter without focusing too much on performance or development comparisons against native development. Let’s begin…
Integrating ARKit with Flutter
Apple provides ARKit SDK to build AR Apps on iOS platforms. This SDK has some key definitions to understand which is out of the scope of this blog post. Please see this article which covers them.
ARKit Materials, Geometry, Node, etc
A great ARKit Flutter plugin is being developed and maintained by Open Source Community. This plugin is actively maintained and provides most of the APIs that would be needed to control ARKit from Flutter. You can also see many code examples for various use cases on the Github page.
Sholo Guti 16 - AR Board Game
Sholo Guti is a 2 player board game originated in Asia. At a high level, each player has 16 soldiers and player who captures all 16 soldiers first wins. To learn more about it, here is the wikipedia link.
Rules of the Game
- Players alternate their turns.
- A player may only use one of their pieces in a turn and must either make a move or perform a capture but not both.
- A piece may move onto any vacant adjacent point along a line.
- A piece may capture an opposing piece by the short leap as in draughts or Alquerque. The piece must be adjacent to the opposing piece, and leap over it onto a vacant point immediately beyond. The leap must be in a straight line and follow the pattern on the board.
- The player who captures all of the other player’s pieces wins.
Creating AR Board
Our Board is a rectangular object divided into grids. In this grid, we have vertexes and edges.
Vertex -> A vertex is a position that can be taken by a soldier.
Edge -> Edges connect the vertexes. They also define the directions in which a piece can move from one vertex to another.
Defining the board
From ARKit perspective, our board is a Node which has a geometry of Box type. In order to keep gaming logic and view logic separate, our ARKit Board object extends from the ARKitNode class.
const BoardTexture = 'models.scnassets/textures/board.jpeg';
class ARKitBoard extends ARKitNode with Board {
ARKitBoard(vector.Vector3 position)
: super(
name: "board",
position: position,
geometry: ARKitBox(
width: boardConfig.width,
height: boardConfig.height,
length: boardConfig.length,
materials: [
ARKitMaterial(diffuse: ARKitMaterialImage(BoardTexture))
]),
rotation: arKitRotationVector);
}
We can also add a texture to the board to make it look more real. This is applied to the materials of the geometry. This is the texture we have used. Any image can be used.

Placing the board on real world in camera
Time to place our first AR object on the board. It is up to us where in the real world we wish to place the board. For our game, we have decided to place it on a plane. This means we detect a flat surface and once it is detected, we place the board on it.
Finding an Anchor on the flat surface
If you are following the sample widget mentioned in the arkit_plugin homepage, you must have noticed the function onARKitViewCreated
. This function also receives an anchor. We can use it to detect a plane and place the board.
void onARKitViewCreated(ARKitController arkitController) {
this.arkitController = arkitController;
this.arkitController.onAddNodeForAnchor = _handleAddAnchor;
}
void _handleAddAnchor(ARKitAnchor anchor) {
if (anchor is ARKitPlaneAnchor) {
this.arkitController.onAddNodeForAnchor = null;
board = ARKitBoard(anchor.center); // position the board in the center of the plane anchor.
await arkitController.add(node, parentNodeName: anchor.nodeName); // add the board node.
}
}
Once the plane anchor is located, all we have to do is place the board with the parent as the anchor as shown above.
Creating Vertex and Edge
We will use Cylinder Geometry to represent our `Vertex` and `Edge` nodes.
There are 2 types of vertexes.
1. Vertex -> A regular vertex
2. Highlighted Vertex -> This will be used to indicate valid positions of our soldiers.
ARKitGeometry vertex() {
return ARKitCylinder(height: 0.001, radius: 0.005, materials: [
ARKitMaterial(diffuse: ARKitMaterialProperty.color(Colors.brown.shade700))
]);
}
ARKitGeometry higlightedVertex() {
return ARKitCylinder(height: 0.002, radius: 0.012, materials: [
ARKitMaterial(
diffuse: ARKitMaterialProperty.color(Colors.brown),
reflective: ARKitMaterialProperty.color(Colors.white30))
]);
}
class ARKitVertex extends ARKitNode {
late Vertex _vertex;
ARKitVertex(Vertex v, {ARKitGeometry? geometry})
: super(
name: v.name,
geometry: geometry != null ? geometry : vertex(),
rotation: vector.Vector4(1, 0, 0, -math.pi / 2)) {
this._vertex = v;
}
}
/**
* Edge Node
*/
class ARKitEdge extends ARKitNode {
ARKitEdge(vector.Vector3 pos1, vector.Vector3 pos2, Direction dir):
super(
geometry: ARKitCylinder(
height: pos1.distanceTo(pos2),
radius: 0.0010,
materials: [
ARKitMaterial(diffuse: ARKitMaterialProperty.color(Colors.brown))
]),
position: (pos1 + pos2) / 2,
rotation: dir.getLineVectorRotation());
}
Placing vertexes and edges on the board
A little bit of math is required here.
We want to have 8 rows and 6 columns on the board. This means that we need to divide our board size evenly across length and breadth and the values we find will be used to draw our vertexes.
We will be placing our Vertexes relative to the position of the board. Hence, positioning them at co-ordinates 0,0 will place them on the bottom left of the board. Use that as a reference, we calculate where we want to place our vertex i.e in which row and column.
Edges will be drawn from one vertex to another. Hence it requires 2 positions. It also requires an angular rotation to have drawn correctly
class BoardConfig {
get width => 0.2 // meters;
get height => 0.3 // meters;
get length => 0.005 // meters;
get rows => 8;
get columns => 6;
}
vector.Vector3 _positionOnBoard(Vertex v) {
final startX = -(board.width / 2);
final startY = -(board.height / 2);
return vector.Vector3(startX + (v.x * board.cellWidth),
startY + (v.y * board.cellHeight), 0.003);
}
drawVertex(Vertex v) async {
final node = ARKitVertex(v);
node.position = _positionOnBoard(v);
await _addNode(node, parentNodeName: board.name);
}
drawEdge(ARKitVertex v1, ARKitVertex v2) async {
final node = ARKitEdge(v1,position, v2.position, getDirection(v1, v2));
await _addNode(node, parentNodeName: board.name);
}
Repeat the same process to place all vertexes and edges.
Creating Soldiers
In our Game, to keep it simple, soldiers are represented by a Box geometry. Hence 16 boxes can be created for each player. The positions are the vertex positions where we wish to place the soldier.
class ARKitSoldier extends ARKitNode {
ARKitSoldier(Player p, vector.Vector3 position)
: super(
name: "soldier_" + p.id + " " + position.x + "x" + position.y,
position: position,
geometry: ARKitBox(
width: 0.005,
height: 0.005,
length: 0.005,
materials: [
ARKitMaterial(diffuse: ARKitMaterialProperty.color(p.color))
])
);
}
User touch detection
Now, we come to the part where we wish to interact with the user. Player should be able to select a soldier and then select a valid vertex where they wish to move the soldier. Thankfully, the arkit_plugin gives a handy callback function to detect touch on any of the AR world nodes.
arkitController.onNodeTap = (String[] nodenames) {
for (final name in nodenames) {
if (name.startsWith("soldier")) {
selectSoldierByName(name);
} else if (name.startsWith("vertex")) {
placeSelectedSoldierToVertex(name);
}
}
}
placeSelectedSoldierToVertex(name) {
final selectedSoldier = this.getSelectedSoldier();
final newVertex = resolveNameToVertex(name);
isValidMove(selectedSoldier, newVertex);
selectedSoldier.position = newVertex.pposition;
}
Once the soldier and vertex are detected, we run our rules algorithm to verify valid moves. If yes, we move the soldier and capture any opposition soldiers in between.
Leave a Reply