Setup HangMan Using CustomPaint Widget

This is the first part of the series where we are developing Hangman game in Flutter. In this post, we will be exploring the CustomPaint widget for setting up our game stage.

At the end of this tutorial, we will have achieved drawing the hanging frame and noose for our game.

HangMan Game Mood
HangMan Game Setting

Introduction

CustomPaint Widget

A CustomPaint widget is useful for drawing shapes and even texts in Flutter app. Although the actual task of drawing is done by CustomPainter, the canvas on which to draw is provided by CustomPaint.

DecisionMentor app

A basic CustomPaint implementation looks like this:

var myCustomPaint = CustomPaint(
  painter: MyPainter(),
  size: myCanvasSize
);

The main ingredient of CustomPaint is the painter object which is an instance of CustomPainter class.

CustomPainter Class

The CustomPainter is an abstract class provided by Flutter framework. Since this is an abstract class, for drawing shapes in our game, we need to create concrete implementation of this class.

class HangManPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // TODO: implement paint
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    return null;
  }
}

The two methods paint and shouldRepaint are used for drawing and repainting.

Now that we have some idea of how CustomPaint can be used, let’s start the game development project.

Project Setup

Let’s start by creating a new Flutter project.

flutter create hangman

Once the project is created, we will remove all of the boilerplate code from main.dart and replace it with following:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'game_stage.dart';

Future<void> main() async {

  WidgetsFlutterBinding.ensureInitialized();

  await SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft]);
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    
    return MaterialApp(
      title: "HangMan",
      home: GameStage(),
    );
  }  
}

Landscape Orientation Mode In Flutter App

For our game, we want a Landscape orientation so that we can access more drawing space. We can force any orientation in Flutter app using the SystemChrome.setPreferredOrientations method.

SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft]);

The GameStage is going to be our main widget where all of the action is going to take place ;).

In another file game_stage.dart, we have:

import 'package:flutter/material.dart';

class GameStage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _GameStage();
  }
}

class _GameStage extends State<GameStage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: Center(child: Text("Hangman Game"),),
      ),
    );
  }
}

If we run the Flutter app right now, we should see the following screen:

Landscape Orientation Flutter
Landscape Orientation Flutter

HangMan CustomPainter Class

Now we start creating a CustomPainter for our game. We will call it HangManPainter.

class HangManPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint();
    paint.color = Colors.grey;
    paint.style = PaintingStyle.fill;

    canvas.drawRect(Rect.fromLTRB(0, size.height, 12, 0), paint);

    canvas.drawRect(Rect.fromLTRB(0, 0, size.width, 12), paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    // we will come back to this later...    
    return false;
  }  
}

The paint method of HangManPainter currently draws two grey rectangles. For drawing the rectangle we are using Rect.fromLTRB method.

This is only a starting point of our painter and we will visit this class later.

For now, let’s see how this painter paints.

Setup HangManPainter With CustomPaint Widget

We will be using the painter in the GameStage widget. So let’s make few changes to this widget.

We want to divide the device area into two parts. The first section is where we will be showing the stick figure. The other section will be used for showing dashes and for guessing.

class _GameStage extends State<GameStage> {
  @override
  Widget build(BuildContext context) {
    var mediaQd = MediaQuery.of(context).size;
    return Scaffold(
      body: Container(
          padding: EdgeInsets.all(24.0),
          width: mediaQd.width,
          height: mediaQd.height,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              Container(
                width: 270,
                height: mediaQd.height,
                padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
                child: CustomPaint(
                  painter: HangManPainter(),
                  size: Size(
                    (270 - 24.0),
                    (mediaQd.height - 24.0),
                  ),
                ),
              ),
              Expanded(
                child: Container(
                  child: Center(child: Text("Hangman")),
                ),
              )
            ],
          )),
    );
  }
}

Run and see how the painter works for now.

Hangman Frame Painter
Hangman Frame Painter

Great, we have a frame now ready to hang the stick figure!

Create A Hanging Noose

Now that we have a working HangManPainter, let’s enhance it a little by adjusting the frame size and adding a noose.

...
@override
  void paint(Canvas canvas, Size size) {
    var paint = Paint();

    paint.color = Colors.grey;
    paint.style = PaintingStyle.fill;
    
    _drawFrame(canvas, size, paint);
    _drawNoose(canvas, size, paint);
  }

  _drawNoose(Canvas canvas, Size size, Paint paint) {
    var nooseStart = Offset(size.width/2, 0);
    var nooseEnd = Offset(size.width/2, size.height/5);
    paint.strokeWidth = 8.0;
    canvas.drawLine(nooseStart, nooseEnd, paint);
  }

  _drawFrame(Canvas canvas, Size size, Paint paint) {
    canvas.drawRect(Rect.fromLTRB(0, size.height, 12, 0), paint);

    canvas.drawRect(Rect.fromLTRB(0, 0, size.width/2, 12), paint);
  }


...

For drawing the hanging noose, we have used canvas.drawLine method.

Add LinearGradient Color For Game Setting

For setting the mood of the game, we can use a LinearGradient of red and blue color. This will help to create a dark blood red feel with a hint of hope 😉

Let’s update the GameStage widget with gradient color mix.

class _GameStage extends State<GameStage> {
  @override
  Widget build(BuildContext context) {
    var mediaQd = MediaQuery.of(context).size;
    return Scaffold(
      body: Container(
          decoration: BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment.topRight,
              end: Alignment.bottomLeft,
              colors: [Colors.blue, Colors.red]
            )
          ),
          padding: EdgeInsets.all(24.0),
          width: mediaQd.width,
          height: mediaQd.height,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,

Wrapping Up

We will wrap up the series at this point for now. So far we have learnt about CustomPaint widget in Flutter and also about CustomPainter class. We have created our own custom painter HangManPainter to draw a setup of hanging frame and noose for the game.

We will add more features on the coming posts.

Full Demo In DartPad

You can see the entire code running in DartPad below.

Next Post

Stick Figure Design With CustomPaint