Flutter×Flame 十字キーで図形を動かし、ネットワーク通信で同期させる

Flameのインストール

dependencies: の項に
flame: ^1.4.0
socket_io_client: ^2.0.0
を追加

flutter pub get を実行

test/ 内のファイル削除(テストコードの書き方知らないです…)

コーディング(クライアント)

main.dart

import 'dart:math' as math;

import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame/palette.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'package:socket_io_client/socket_io_client.dart';

void main() {
  runApp(
    GameWidget(
      game: MyGame(),
    ),
  );
}

final Vector2 inputVec = Vector2(0, 0);

class EmitData {
  double? angle = 0;
  String token = "token";
  double? positionX = 0;
  double? positionY = 0;

  EmitData();

  EmitData.fromJson(Map<String, dynamic> json)
      : angle = json['angle'],
        token = json['token'],
        positionX = json['positionX'],
        positionY = json['positionY'];
}

class Gateway {
  late final Socket _socket;
  late String mytoken = "";
  late Square mysquare;
  late SquareEnemy yoursquare;
  late EmitData emitData = EmitData();

  Gateway() {
    _socket = io(
      "https://xxxxxxxxxxxxxx/",
      OptionBuilder()
        .setTransports(['websocket'])
        .disableAutoConnect()
        .build()
    );

    _socket.onConnect((data) {
      _socket.on("token", (data) {
        emitData = EmitData.fromJson(data);
        mytoken = emitData.token;
      });
    });

    _socket.on("member-post", (msg) => {
      emitData = EmitData.fromJson(msg),
      if(emitData.token == mytoken){

      } else {
        yoursquare.position.x = emitData.positionX!,
        yoursquare.position.y = emitData.positionY!,
        yoursquare.angle = emitData.angle!
      }
    });

    _socket.connect();
  }

}

/// This example simply adds a rotating white square on the screen.
/// If you press on a square, it will be removed.
/// If you press anywhere else, another square will be added.
class MyGame extends FlameGame with HasKeyboardHandlerComponents {
  final Gateway _gateway = Gateway()
    ..mysquare = Square(Vector2(100, 200))
    ..yoursquare = SquareEnemy(Vector2(100, 200));

  @override
  Future<void> onLoad() async {
    add(_gateway.mysquare);
    add(_gateway.yoursquare);
  }

  @override
  void update(double dt) {
    super.update(dt);
    _gateway._socket.emit("post", {
      "angle": _gateway.mysquare.angle,
      "token": _gateway.mytoken,
      "positionX": _gateway.mysquare.position.x,
      "positionY": _gateway.mysquare.position.y
    });
    
  }

  @override
  @mustCallSuper
  KeyEventResult onKeyEvent(
    RawKeyEvent event,
    Set<LogicalKeyboardKey> keysPressed,
  ) {
    super.onKeyEvent(event, keysPressed);

    return KeyEventResult.handled;
  }
}

class Square extends PositionComponent {
  static const speed = 0.75;
  static const squareSize = 128.0;

  static Paint white = BasicPalette.white.paint();
  static Paint red = BasicPalette.red.paint();
  static Paint blue = BasicPalette.blue.paint();

  Square(Vector2 position) : super(position: position);

  @override
  void render(Canvas c) {
    c.drawRect(size.toRect(), white);
    c.drawRect(const Rect.fromLTWH(0, 0, 3, 3), red);
    c.drawRect(Rect.fromLTWH(width / 2, height / 2, 3, 3), blue);
  }

  @override
  void update(double dt) {
    super.update(dt);
    if(inputVec.y <= 0){
      angle += speed * dt * inputVec.x; 
    }else{
      angle -= speed * dt * inputVec.x;
    }
    angle %= 2 * math.pi;
    position.x += math.cos(angle) * inputVec.y * 2 * speed;
    position.y += math.sin(angle) * inputVec.y * 2 * speed;
    
  }

  @override
  Future<void> onLoad() async {
    // super.onLoad();

    size.setValues(squareSize, squareSize);
    anchor = Anchor.center;
    add(
      KeyboardListenerComponent(
        keyDown: {
          LogicalKeyboardKey.arrowDown: (keysPressed) {
            inputVec.y = 1;

            return true;
          },
          LogicalKeyboardKey.arrowUp: (keysPressed) {
            inputVec.y = -1;

            return true;
          },
          LogicalKeyboardKey.arrowRight: (keysPressed) {
            inputVec.x = 1;

            return true;
          },
          LogicalKeyboardKey.arrowLeft: (keysPressed) {
            inputVec.x = -1;

            return true;
          },
        },
        keyUp: {
          LogicalKeyboardKey.arrowDown: (keysPressed) {
            inputVec.y = 0;

            return true;
          },
          LogicalKeyboardKey.arrowUp: (keysPressed) {
            inputVec.y = 0;

            return true;
          },
          LogicalKeyboardKey.arrowRight: (keysPressed) {
            inputVec.x = 0;

            return true;
          },
          LogicalKeyboardKey.arrowLeft: (keysPressed) {
            inputVec.x = 0;

            return true;
          },
        },
      ),
    );

    return super.onLoad();
  }

}

class SquareEnemy extends PositionComponent {
  static const speed = 0.75;
  static const squareSize = 128.0;

  static Paint white = BasicPalette.white.paint();
  static Paint red = BasicPalette.red.paint();
  static Paint blue = BasicPalette.blue.paint();

  SquareEnemy(Vector2 position) : super(position: position);

  @override
  void render(Canvas c) {
    c.drawRect(size.toRect(), white);
    c.drawRect(const Rect.fromLTWH(0, 0, 3, 3), red);
    c.drawRect(Rect.fromLTWH(width / 2, height / 2, 3, 3), blue);
  }

  @override
  Future<void> onLoad() async {
    size.setValues(squareSize, squareSize);
    anchor = Anchor.center;
    return super.onLoad();
  }
}

解説…?

GameWigetっていうのが、Flameで追加されたWidget。
FlameGameを継承したクラスを渡すっぽい。

void main() {
  runApp(
    GameWidget(
      game: MyGame(),
    ),
  );
}

with HasKeyboardHandlerComponents をつけてGameクラスつくるとキーボード入力を受付できる。
よくわからんけどonKeyEentを下記のような感じでoverrideすると他のコンポーネントでキーボード入力受信する。

class MyGame extends FlameGame with HasKeyboardHandlerComponents {
..
@override
  @mustCallSuper
  KeyEventResult onKeyEvent(
    RawKeyEvent event,
    Set<LogicalKeyboardKey> keysPressed,
  ) {
    super.onKeyEvent(event, keysPressed);

    return KeyEventResult.handled;
  }
}

PositionComponentを継承して作ったSquareクラス。
プレイヤー。
PositionComponentはComponentクラスを継承していて、(ComponentはonLoad()とかupdate()が実装されていてGameクラスから呼び出される。)
positionとかangleを持ってる。
SpriteComponentというのもあるらしく、こちらは画像ファイルとか渡すとそれを表示するらしい。
今回はrender()に直接図形を描画する処理を書いている。
onLoad()内でKeyboardListenerComponentをaddしておくとキーが入力されたときの処理を書ける。
今回は2次元ベクトルを増減させている。
update()内では上記のベクトルをもとに移動処理を書いている。毎フレーム呼び出される。

class Square extends PositionComponent {
  static const speed = 0.75;
  static const squareSize = 128.0;

  static Paint white = BasicPalette.white.paint();
  static Paint red = BasicPalette.red.paint();
  static Paint blue = BasicPalette.blue.paint();

  Square(Vector2 position) : super(position: position);

  @override
  void render(Canvas c) {
    c.drawRect(size.toRect(), white);
    c.drawRect(const Rect.fromLTWH(0, 0, 3, 3), red);
    c.drawRect(Rect.fromLTWH(width / 2, height / 2, 3, 3), blue);
  }

  @override
  void update(double dt) {
    super.update(dt);
    if(inputVec.y <= 0){
      angle += speed * dt * inputVec.x; 
    }else{
      angle -= speed * dt * inputVec.x;
    }
    angle %= 2 * math.pi;
    position.x += math.cos(angle) * inputVec.y * 2 * speed;
    position.y += math.sin(angle) * inputVec.y * 2 * speed;
    
  }

  @override
  Future<void> onLoad() async {
    // super.onLoad();

    size.setValues(squareSize, squareSize);
    anchor = Anchor.center;
    add(
      KeyboardListenerComponent(
        keyDown: {
          LogicalKeyboardKey.arrowDown: (keysPressed) {
            inputVec.y = 1;

            return true;
          },
          LogicalKeyboardKey.arrowUp: (keysPressed) {
            inputVec.y = -1;

            return true;
          },
          LogicalKeyboardKey.arrowRight: (keysPressed) {
            inputVec.x = 1;

            return true;
          },
          LogicalKeyboardKey.arrowLeft: (keysPressed) {
            inputVec.x = -1;

            return true;
          },
        },
        keyUp: {
          LogicalKeyboardKey.arrowDown: (keysPressed) {
            inputVec.y = 0;

            return true;
          },
          LogicalKeyboardKey.arrowUp: (keysPressed) {
            inputVec.y = 0;

            return true;
          },
          LogicalKeyboardKey.arrowRight: (keysPressed) {
            inputVec.x = 0;

            return true;
          },
          LogicalKeyboardKey.arrowLeft: (keysPressed) {
            inputVec.x = 0;

            return true;
          },
        },
      ),
    );

    return super.onLoad();
  }

}

SquareEnemyは上のクラスの移動関連の処理を消したもの。

Gatewayクラスでサーバーからのイベントに基づいた処理を書いてる。
MyGameクラスないでインスタンス化しており、Gatewayはプレイヤーとエネミーを持つ。(相手のtokenのメッセージを受信したら、エネミースクエアの位置を動かすのもこいつ)

class Gateway {
  late final Socket _socket;
  late String mytoken = "";
  late Square mysquare;
  late SquareEnemy yoursquare;
  late EmitData emitData = EmitData();

  Gateway() {
    _socket = io(
      "https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/",
      OptionBuilder()
        .setTransports(['websocket'])
        .disableAutoConnect()
        .build()
    );

    _socket.onConnect((data) {
      _socket.on("token", (data) {
        emitData = EmitData.fromJson(data);
        mytoken = emitData.token;
      });
    });

    _socket.on("member-post", (msg) => {
      emitData = EmitData.fromJson(msg),
      if(emitData.token == mytoken){

      } else {
        yoursquare.position.x = emitData.positionX!,
        yoursquare.position.y = emitData.positionY!,
        yoursquare.angle = emitData.angle!
      }
    });

    _socket.connect();
  }

}

ちなみに、MyGameのupdateで毎フレームサーバーに自身の位置とかを送信している。

final Gateway _gateway = Gateway()
    ..mysquare = Square(Vector2(100, 200))
    ..yoursquare = SquareEnemy(Vector2(100, 200));

  @override
  Future<void> onLoad() async {
    add(_gateway.mysquare);
    add(_gateway.yoursquare);
  }

  @override
  void update(double dt) {
    super.update(dt);
    _gateway._socket.emit("post", {
      "angle": _gateway.mysquare.angle,
      "token": _gateway.mytoken,
      "positionX": _gateway.mysquare.position.x,
      "positionY": _gateway.mysquare.position.y
    });
    
  }

サーバーサイド

Flutter×Socket.IO チャットアプリをつくる|Kijin Kamukura|note

前回のをそのまま流用した。

成果物

備考

Flutter 3.5.0-12.0.pre.145 • channel master • https://github.com/flutter/flutter.git
Framework • revision a1289a4135 (10 days ago) • 2022-11-07 06:46:25 -0500
Engine • revision 891d4a3577
Tools • Dart 2.19.0 (build 2.19.0-374.0.dev) • DevTools 2.19.0

この記事が気に入ったらサポートをしてみませんか?