2014-01-28

A Tutorial; Step-by-step

I've been planning to add a tutorial to my game from the beginning, and finally got around to really working on it a few weeks ago. Similar to my previous post about simple solutions, I found that once I tinkered with ideas enough and came up with a clear idea of how to implement it, my tutorial code just fell into place quite smoothly and fit satisfyingly into the existing architecture.

Making a point

I decided that the best way to communicate how to play the game was to have an image of a finger moving on the screen indicating touch actions. You can see an example of this in the image to the right. I created the hand image by taking a photo of a hand (my eldest daughter's) and then manipulating it a bit to flatten the colors and such. In the tutorial it is opaque when touching the screen (the left image), or mostly transparent when not touching the screen (the right image).

After creating the finger I needed a way to move it around the screen easily and also initiate the touch events. For this I created a Finger class that extends Symbol (see my previous post for a discussion on this) and holds the image of the finger. This class also has new animation functionality mostly implemented in the methods shown below.

BeginAnimation is called once for each single animation step (e.g., moving from point A to point B with the finger touching the screen or not, as indicated). This animation is then handled as part of the normal Animate method, which is called once for each Widget during the main animation loop, by calling DoFingerAnimation. As you can see it mostly just updates the finger's position and, once complete, calls the _fingerCompletionAction.

public void BeginAnimation(Point start, Point finish, bool isTouchingScreen, TimeSpan duration, Func<bool> completionChecker, Action completionAction)
{
    _fingerMoveStartTime = DateTime.Now;
    _fingerMoveDuration = duration;
    AnimationStartPosition = start;
    AnimationFinishPosition = finish;
    IsTouchingScreen = isTouchingScreen;
    _fingerCompletionChecker = completionChecker;
    _fingerCompletionAction = completionAction;
}

private void DoFingerAnimation()
{
    TimeSpan elapsed = DateTime.Now - _fingerMoveStartTime;
    if(elapsed < _fingerMoveDuration)
    {
        Position = Animator.Interpolate(
            AnimationStartPosition,
            AnimationFinishPosition,
            _fingerMoveDuration.TotalSeconds,
            elapsed.TotalSeconds,
            Animator.Curve.Linear
        );
        AnimationCurrentPosition = Position;
    }
    else if(IsBeingAnimated && _fingerCompletionChecker())
    {
        _fingerMoveStartTime = DateTime.MinValue;
        _fingerCompletionAction();
    }
}

Stepping it up

So, now that I can perform a single step of an animation controlling the finger, I need to string these together into multiple steps that show a complete tutorial lesson. To do this I created a couple of data holders within my Tutorial class. The first, Step, represents a single step of the tutorial and performs a single finger animation movement. The second, Lesson, holds all of the data for a single tutorial lesson including the game elements to show on the screen and the sequence of steps.

One thing to note, there is a slightly confusing use of the term "completion checker" here, since it is used twice. It basically serves the same purpose for two different levels of the lesson. Inside Step it is used to determine if that step should end. Of course the step has a set duration, but even after that duration there can be other conditions that must be met (see the actual lesson examples later). Similarly, in Lesson this is used to determine if the lesson is complete.

private struct Step
{
    public Step(double destinationX, double destinationY, bool touchScreen, double duration, Func<Tutorial, bool> completionChecker)
    {
        Finish = new Point((float)destinationX, (float)destinationY);
        TouchScreen = touchScreen;
        Duration = TimeSpan.FromSeconds(duration);
        CompletionChecker = completionChecker ?? (foo => { return true; });
    }

    public Point Finish;
    public bool TouchScreen;
    public TimeSpan Duration;
    public Func<Tutorial, bool> CompletionChecker;
}

private struct Lesson
{
    public int Width;
    public int Height;
    public IEnumerable<Point> Goals;
    public IEnumerable<ShipInfo> Ships;
    public IEnumerable<Tuple<int, int, WallLocation>> Walls;
    public Func<Tutorial, bool> CompletionChecker;
    public IEnumerable<Step> Steps;
}

A lesson plan

Fortunately I was able to use a combination of the yield return technique for enumerations and C#'s object initializers to compactly define individual lessons. I do this statically and populate an array to hold them all.

private static Lesson[] All = Tutorials.ToArray();

private static IEnumerable<Lesson> Tutorials
{
    get
    {
        int width = 4;
        int height = 6;

        // Create one simple diagonal.
        yield return new Lesson
        {
            Width = width,
            Height = height,
            Goals = new Point[] { new Point(width - 2, height - 2) },
            Ships = new ShipInfo[] { new ShipInfo(ShipType.Hero, new Point(0.5f, 0.5f), Direction.East, 0.025f) },
            Walls = new Tuple<int, int, WallLocation>[0],
            CompletionChecker = tutorial => { return tutorial.AchievedAllGoals; },
            Steps = new Step[] {
                new Step(width * 0.6, height * 0.4, false, 3, null),
                new Step(width - 2, 0, false, 2.5, null),
                new Step(width - 1, 1, true, 1.5, tutorial => { return tutorial.Ships.First().Position.X < width - 2.5; }),
                new Step(width - 0.5, 1.5, false, 1.5, null)
            }
        };
        .
        .
        .

Pulling apart this first Lesson, the interesting part is the 3rd step that has the non-null completion check. This check ensures that the Ship is far enough to the left before taking the finger off of the screen, and therefore completing the diagonal. Without doing this the ship could end up on the wrong side of the diagonal and not bounce to where it is supposed to.

There are a number of interim lessons I'm not including here, but one interesting one (shown below) is the lesson showing how to pause the game, which is done by swiping all the way across the screen horizontally in either direction. The interesting part here is that I needed to show the pause symbol, and then the continue symbol. To do this I cheated a little in two ways. First, in the step before I want the appropriate symbol to be visible, I used the completion check to create the needed symbol, although it's alpha was initially set to 100% transparent. This is done via the CreatePauseSymbol and CreateContinueSymbol methods, not shown here. Second, also not shown here, I adjust the transparency of the pause symbol to become more opaque as the finger completes its animation. This was a little hackish, but worked out pretty well.

        .
        .
        .
        // How to pause.
        yield return new Lesson
        {
            Width = width,
            Height = height,
            Goals = new Point[] { new Point(width - 2, height - 2) },
            Ships = new ShipInfo[] { new ShipInfo(ShipType.Hero, new Point(0.5f, 0.5f), Direction.South, 0.045f) },
            Walls = new Tuple<int, int, WallLocation>[0],
            CompletionChecker = tutorial => { return true; },
            Steps = new Step[] {
                new Step(width * 0.6, height * 0.8, false, 2, null),
                new Step(0, height * 0.45, false, 1, tutorial => tutorial.CreatePauseSymbol()),
                new Step(width, height * 0.55, true, 3, tutorial => tutorial.CreateContinueSymbol()),
                new Step(width - 1.5f, height - 1.5f, false, 1.5, null),
                new Step(width * 0.5, height * 0.5, false, 1.5, null),
                new Step(width * 0.5, height * 0.5, true, 0.05, tutorial => tutorial.RemoveContinueSymbol()),
                new Step(width * 0.65, height * 0.65, false, 0.5, null),
                new Step(0, height - 2, false, 0.75, null),
                new Step(1, height - 1, true, 0.75, tutorial => { return tutorial.Ships.First().Position.Y < height - 2; }),
                new Step(width * 0.75, height * 0.55, false, 1, null),
            }
        };
    }
}

Linking them together

Finally, now that the lessons are defined, I need to do two more things: make the finger's touch actually behave like a normal touch in a normal game, and queue up the steps so they play in order. The first was pretty easy by just calling the existing touch handlers with the appropriate touch points. The second also turned out particularly well because I used the built in onComplete action in the finger animations to recursively call a method that dequeues each successive step.

private void DoTutorialSteps(Queue<Step> queue)
{
    if(queue.Count > 0)
    {
        Step step = queue.Dequeue();
        Action onComplete = step.TouchScreen
            ? new Action(() =>
                {
                    HandleTouch(BoardToScreen(_finger.AnimationStartPosition), BoardToScreen(_finger.AnimationCurrentPosition), true);
                    base.HandleBoardTouch(_finger.AnimationStartPosition, _finger.AnimationCurrentPosition, true);
                    DoTutorialSteps(queue);
                })
            : new Action(() => { DoTutorialSteps(queue); });
        _finger.BeginAnimation(_finger.Position, step.Finish, step.TouchScreen, step.Duration, () => step.CompletionCheck(this), onComplete);
    }
}

I'm now almost done with the tutorial and have only one more lesson to add.

Next time: Alpha layering.

No comments:

Post a Comment