Flutter×Flame 十字キーで図形を動かし、ネットワーク通信で同期させる
Flameのインストール
dependencies: の項に
flame: ^1.4.0
socket_io_client: ^2.0.0
を追加
flutter pub get を実行
test/ 内のファイル削除(テストコードの書き方知らないです…)
コーディング(クライアント)
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
この記事が気に入ったらサポートをしてみませんか?