No files matched your search
@@ -209,12 +209,12 @@ curl '<SUPABASE_URL>/rest/v1/todos' \
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
JS Reference: [select()](/reference/javascript/select),
|
||||
[insert()](/reference/javascript/insert),
|
||||
[update()](/reference/javascript/update),
|
||||
[upsert()](/reference/javascript/upsert),
|
||||
[delete()](/reference/javascript/delete),
|
||||
[rpc()](/reference/javascript/rpc) (call Postgres functions).
|
||||
JS Reference: [select()](/docs/reference/javascript/select),
|
||||
[insert()](/docs/reference/javascript/insert),
|
||||
[update()](/docs/reference/javascript/update),
|
||||
[upsert()](/docs/reference/javascript/upsert),
|
||||
[delete()](/docs/reference/javascript/delete),
|
||||
[rpc()](/docs/reference/javascript/rpc) (call Postgres functions).
|
||||
|
||||
### GraphQL API
|
||||
|
||||
|
||||
@@ -0,0 +1,839 @@
|
||||
---
|
||||
title: How to build a real-time multiplayer game with Flutter Flame
|
||||
description: Build a real-time multiplayer game using Flutter, Flame, and Supabase realtime.
|
||||
author: tyler_shukert
|
||||
image: flutter-realtime-game/flutter-flame-realtime-multiplayer-game.png
|
||||
thumb: flutter-realtime-game/flutter-flame-realtime-multiplayer-game.png
|
||||
tags:
|
||||
- flutter
|
||||
- mobile
|
||||
- realtime
|
||||
date: '2023-02-14'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
Flutter is a UI library to build apps that run on any platform, but it can also build interactive games thanks to an open-source game engine built on top of Flutter called [Flame](https://flame-engine.org/). Flame takes care of things like collision detection or loading image sprites to bring game development to all the Flutter devs. We can take it a step further to introduce real-time communication features so that players can play against each other in real-time.
|
||||
|
||||
In this article, we will use Flutter, Flame, and Supabase's real-time features to build a real-time multiplayer shooting game. You can find the complete code of this tutorial [here](https://github.com/supabase/supabase/tree/master/examples/realtime/examples/flutter-multiplayer-shooting-game).
|
||||
|
||||
## Overview of the final game
|
||||
|
||||
<video width="99%" loop="" muted="" playsInline="" controls="true">
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/flutter-realtime-game/playing-realtime-game.mp4"
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
|
||||
The game is a simple shooting game. Each player has a UFO, and you can move it by dragging your finger across the screen. The UFO will emit bullets automatically in three directions, and the objective of the game is to hit the opponents with bullets before your UFO gets destroyed by the opponent’s bullets. The position and the health points are synced using a low-latency web socket connection provided by Supabase.
|
||||
|
||||
Before entering the main game, there is a lobby where you can wait for other players to show up. Once another player shows up, you can hit start, which will kick off the game on both ends.
|
||||
|
||||
We will first build the Flutter widgets used to build the basic UI, then build the Flame game and finally handle the network connection to share the data between connected clients.
|
||||
|
||||
## Build the App
|
||||
|
||||
### Step 1. Create the Flutter App
|
||||
|
||||
We will start out by creating the Flutter app. Open your terminal and create a new app named with the following command.
|
||||
|
||||
```bash
|
||||
flutter create flame_realtime_shooting
|
||||
```
|
||||
|
||||
Open the created app with your favorite IDE and let’s get started with coding!
|
||||
|
||||
### Step 2. Building the Flutter widgets
|
||||
|
||||
We will have a simple directory structure to build this app. Since we only have a few widgets, we will just add them inside `main.dart` file.
|
||||
|
||||
```
|
||||
├── lib/
|
||||
| ├── game/
|
||||
│ │ ├── game.dart
|
||||
│ │ ├── player.dart
|
||||
│ │ └── bullet.dart
|
||||
│ └── main.dart
|
||||
```
|
||||
|
||||
### Create the Main Game Page
|
||||
|
||||
We will create minimal Flutter widgets for this app as most of the game logic will be handled in the Flame Game classes later. Our game will have a single page with two dialogs before the game starts and after the game ends. The page will simply contain the [GameWidget](https://docs.flame-engine.org/1.6.0/flame/game_widget.html) while displaying a nice background image. We will make it a StatefulWidget, because we will add methods to handle sending and receiving real-time data later on. Add the following to `main.dart` file.
|
||||
|
||||
```dart
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_realtime_shooting/game/game.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
title: 'UFO Shooting Game',
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: GamePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GamePage extends StatefulWidget {
|
||||
const GamePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<GamePage> createState() => _GamePageState();
|
||||
}
|
||||
|
||||
class _GamePageState extends State<GamePage> {
|
||||
late final MyGame _game;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Image.asset('assets/images/background.jpg', fit: BoxFit.cover),
|
||||
GameWidget(game: _game),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initialize();
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
_game = MyGame(
|
||||
onGameStateUpdate: (position, health) async {
|
||||
// TODO: handle gmae state update here
|
||||
},
|
||||
onGameOver: (playerWon) async {
|
||||
// TODO: handle when the game is over here
|
||||
},
|
||||
);
|
||||
|
||||
// await for a frame so that the widget mounts
|
||||
await Future.delayed(Duration.zero);
|
||||
|
||||
if (mounted) {
|
||||
_openLobbyDialog();
|
||||
}
|
||||
}
|
||||
|
||||
void _openLobbyDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
return _LobbyDialog(
|
||||
onGameStarted: (gameId) async {
|
||||
// handle game start here
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You will see some errors, because we are importing some files that we haven’t created yet, but don’t worry, because we will get to it soon.
|
||||
|
||||
### Create the Lobby Dialog
|
||||
|
||||

|
||||
|
||||
Lobby dialog class on the surface is a simple Alert dialog, but will hold its own states like the list of players that are waiting at the lobby. We will also add some classes to handle the presence data on the lobby later on, but for now we will just have a simple AlertDialog. Add the following code at the end of main.dart file.
|
||||
|
||||
```dart
|
||||
class _LobbyDialog extends StatefulWidget {
|
||||
const _LobbyDialog({
|
||||
required this.onGameStarted,
|
||||
});
|
||||
|
||||
final void Function(String gameId) onGameStarted;
|
||||
|
||||
@override
|
||||
State<_LobbyDialog> createState() => _LobbyDialogState();
|
||||
}
|
||||
|
||||
class _LobbyDialogState extends State<_LobbyDialog> {
|
||||
final List<String> _userids = [];
|
||||
bool _loading = false;
|
||||
|
||||
/// TODO: assign unique identifier for the user
|
||||
final myUserId = '';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Lobby'),
|
||||
content: _loading
|
||||
? const SizedBox(
|
||||
height: 100,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
: Text('${_userids.length} users waiting'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _userids.length < 2
|
||||
? null
|
||||
: () async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
});
|
||||
|
||||
// TODO: notify the other player the start of the game
|
||||
},
|
||||
child: const Text('start'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3. Building the Flame components
|
||||
|
||||
### Creating the FlameGame
|
||||
|
||||
Now the fun part starts! We will start out by creating our game class. We create a `MyGame` class that extends the [FlameGame](https://docs.flame-engine.org/1.6.0/flame/game.html) class. `FlameGame` takes care of collision detection and pan-detection, and it will also be the parent of all the components that we will add to the game. The game contains 2 components, `Player` and `Bullet`. `MyGame ` is a class that wraps around all components of the game and can control the child components.
|
||||
|
||||

|
||||
|
||||
Let’s add flame to our app by adding the following to our `pubspec.yaml` file to install flame.
|
||||
|
||||
```yaml
|
||||
flame: ^1.6.0
|
||||
```
|
||||
|
||||
We can then create `MyGame` class. Add the following code in `lib/game.dart` file.
|
||||
|
||||
```dart
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/events.dart';
|
||||
import 'package:flame/image_composition.dart' as flame_image;
|
||||
import 'package:flame_realtime_shooting/game/bullet.dart';
|
||||
import 'package:flame_realtime_shooting/game/player.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MyGame extends FlameGame with PanDetector, HasCollisionDetection {
|
||||
MyGame({
|
||||
required this.onGameOver,
|
||||
required this.onGameStateUpdate,
|
||||
});
|
||||
|
||||
static const _initialHealthPoints = 100;
|
||||
|
||||
/// Callback to notify the parent when the game ends.
|
||||
final void Function(bool didWin) onGameOver;
|
||||
|
||||
/// Callback for when the game state updates.
|
||||
final void Function(
|
||||
Vector2 position,
|
||||
int health,
|
||||
) onGameStateUpdate;
|
||||
|
||||
/// `Player` instance of the player
|
||||
late Player _player;
|
||||
|
||||
/// `Player` instance of the opponent
|
||||
late Player _opponent;
|
||||
|
||||
bool isGameOver = true;
|
||||
|
||||
int _playerHealthPoint = _initialHealthPoints;
|
||||
|
||||
late final flame_image.Image _playerBulletImage;
|
||||
late final flame_image.Image _opponentBulletImage;
|
||||
|
||||
@override
|
||||
Color backgroundColor() {
|
||||
return Colors.transparent;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void>? onLoad() async {
|
||||
final playerImage = await images.load('player.png');
|
||||
_player = Player(isMe: true);
|
||||
final spriteSize = Vector2.all(Player.radius * 2);
|
||||
_player.add(SpriteComponent(sprite: Sprite(playerImage), size: spriteSize));
|
||||
add(_player);
|
||||
|
||||
final opponentImage = await images.load('opponent.png');
|
||||
_opponent = Player(isMe: false);
|
||||
_opponent.add(SpriteComponent.fromImage(opponentImage, size: spriteSize));
|
||||
add(_opponent);
|
||||
|
||||
_playerBulletImage = await images.load('player-bullet.png');
|
||||
_opponentBulletImage = await images.load('opponent-bullet.png');
|
||||
|
||||
await super.onLoad();
|
||||
}
|
||||
|
||||
@override
|
||||
void onPanUpdate(DragUpdateInfo info) {
|
||||
_player.move(info.delta.global);
|
||||
final mirroredPosition = _player.getMirroredPercentPosition();
|
||||
onGameStateUpdate(mirroredPosition, _playerHealthPoint);
|
||||
super.onPanUpdate(info);
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
if (isGameOver) {
|
||||
return;
|
||||
}
|
||||
for (final child in children) {
|
||||
if (child is Bullet && child.hasBeenHit && !child.isMine) {
|
||||
_playerHealthPoint = _playerHealthPoint - child.damage;
|
||||
final mirroredPosition = _player.getMirroredPercentPosition();
|
||||
onGameStateUpdate(mirroredPosition, _playerHealthPoint);
|
||||
_player.updateHealth(_playerHealthPoint / _initialHealthPoints);
|
||||
}
|
||||
}
|
||||
if (_playerHealthPoint <= 0) {
|
||||
endGame(false);
|
||||
}
|
||||
}
|
||||
|
||||
void startNewGame() {
|
||||
isGameOver = false;
|
||||
_playerHealthPoint = _initialHealthPoints;
|
||||
|
||||
for (final child in children) {
|
||||
if (child is Player) {
|
||||
child.position = child.initialPosition;
|
||||
} else if (child is Bullet) {
|
||||
child.removeFromParent();
|
||||
}
|
||||
}
|
||||
|
||||
_shootBullets();
|
||||
}
|
||||
|
||||
/// shoots out bullets form both the player and the opponent.
|
||||
///
|
||||
/// Calls itself every 500 milliseconds
|
||||
Future<void> _shootBullets() async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
/// Player's bullet
|
||||
final playerBulletInitialPosition = Vector2.copy(_player.position)
|
||||
..y -= Player.radius;
|
||||
final playerBulletVelocities = [
|
||||
Vector2(0, -100),
|
||||
Vector2(60, -80),
|
||||
Vector2(-60, -80),
|
||||
];
|
||||
for (final bulletVelocity in playerBulletVelocities) {
|
||||
add((Bullet(
|
||||
isMine: true,
|
||||
velocity: bulletVelocity,
|
||||
image: _playerBulletImage,
|
||||
initialPosition: playerBulletInitialPosition,
|
||||
)));
|
||||
}
|
||||
|
||||
/// Opponent's bullet
|
||||
final opponentBulletInitialPosition = Vector2.copy(_opponent.position)
|
||||
..y += Player.radius;
|
||||
final opponentBulletVelocities = [
|
||||
Vector2(0, 100),
|
||||
Vector2(60, 80),
|
||||
Vector2(-60, 80),
|
||||
];
|
||||
for (final bulletVelocity in opponentBulletVelocities) {
|
||||
add((Bullet(
|
||||
isMine: false,
|
||||
velocity: bulletVelocity,
|
||||
image: _opponentBulletImage,
|
||||
initialPosition: opponentBulletInitialPosition,
|
||||
)));
|
||||
}
|
||||
|
||||
_shootBullets();
|
||||
}
|
||||
|
||||
void updateOpponent({required Vector2 position, required int health}) {
|
||||
_opponent.position = Vector2(size.x * position.x, size.y * position.y);
|
||||
_opponent.updateHealth(health / _initialHealthPoints);
|
||||
}
|
||||
|
||||
/// Called when either the player or the opponent has run out of health points
|
||||
void endGame(bool playerWon) {
|
||||
isGameOver = true;
|
||||
onGameOver(playerWon);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
There is a lot going here, so let’s break it down. Within the `onLoad` method, we are loading all of the sprites used throughout the game. Then we are adding the player and opponent component.
|
||||
|
||||
Within `onPanUpdate`, we handle the user dragging on the screen. Note that we are calling the `onGameStateUpdate` callback to pass the player’s position so that we can share it to the opponent’s client later when we handle network connections. On the other hand, we have the `updateOpponent` method, which is used to update the opponent’s state with the information coming in from the network. We will also add logic to call it from the Flutter widgets later.
|
||||
|
||||
Upon starting the game, `_shootBullets()` is called, which shoots out bullets both from the player and the opponent. `_shootBullets()` is a recursive function that calls itself every 500 milliseconds. If the bullet hits the player, it is caught inside the `udpate()` method, which is called on every frame. There we calculate the new player’s health points.
|
||||
|
||||
### Creating the Player Component
|
||||
|
||||
`Player` component has the UFO sprite and represents the player and the opponent. It extends the `PositionComponent` from Flame. Add the following in `lib/player.dart`
|
||||
|
||||
```dart
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_realtime_shooting/game/bullet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Player extends PositionComponent with HasGameRef, CollisionCallbacks {
|
||||
Vector2 velocity = Vector2.zero();
|
||||
|
||||
late final Vector2 initialPosition;
|
||||
|
||||
Player({required bool isMe}) : _isMyPlayer = isMe;
|
||||
|
||||
/// Whether it's me or the opponent
|
||||
final bool _isMyPlayer;
|
||||
|
||||
static const radius = 30.0;
|
||||
|
||||
@override
|
||||
Future<void>? onLoad() async {
|
||||
anchor = Anchor.center;
|
||||
width = radius * 2;
|
||||
height = radius * 2;
|
||||
|
||||
final initialX = gameRef.size.x / 2;
|
||||
initialPosition = _isMyPlayer
|
||||
? Vector2(initialX, gameRef.size.y * 0.8)
|
||||
: Vector2(initialX, gameRef.size.y * 0.2);
|
||||
position = initialPosition;
|
||||
|
||||
add(CircleHitbox());
|
||||
add(_Gauge());
|
||||
await super.onLoad();
|
||||
}
|
||||
|
||||
void move(Vector2 delta) {
|
||||
position += delta;
|
||||
}
|
||||
|
||||
void updateHealth(double healthLeft) {
|
||||
for (final child in children) {
|
||||
if (child is _Gauge) {
|
||||
child._healthLeft = healthLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
|
||||
super.onCollision(intersectionPoints, other);
|
||||
if (other is Bullet && _isMyPlayer != other.isMine) {
|
||||
other.hasBeenHit = true;
|
||||
other.removeFromParent();
|
||||
}
|
||||
}
|
||||
|
||||
/// returns the mirrored percent position of the player
|
||||
/// to be broadcasted to other clients
|
||||
Vector2 getMirroredPercentPosition() {
|
||||
final mirroredPosition = gameRef.size - position;
|
||||
return Vector2(mirroredPosition.x / gameRef.size.x,
|
||||
mirroredPosition.y / gameRef.size.y);
|
||||
}
|
||||
}
|
||||
|
||||
class _Gauge extends PositionComponent {
|
||||
double _healthLeft = 1.0;
|
||||
|
||||
@override
|
||||
FutureOr<void> onLoad() {
|
||||
final playerParent = parent;
|
||||
if (playerParent is Player) {
|
||||
width = playerParent.width;
|
||||
height = 10;
|
||||
anchor = Anchor.centerLeft;
|
||||
position = Vector2(0, 0);
|
||||
}
|
||||
return super.onLoad();
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
super.render(canvas);
|
||||
canvas.drawRect(
|
||||
Rect.fromPoints(
|
||||
const Offset(0, 0),
|
||||
Offset(width, height),
|
||||
),
|
||||
Paint()..color = Colors.white);
|
||||
canvas.drawRect(
|
||||
Rect.fromPoints(
|
||||
const Offset(0, 0),
|
||||
Offset(width * _healthLeft, height),
|
||||
),
|
||||
Paint()
|
||||
..color = _healthLeft > 0.5
|
||||
? Colors.green
|
||||
: _healthLeft > 0.25
|
||||
? Colors.orange
|
||||
: Colors.red);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can see that it has a `_isMyPlayer` property, which is true for the player and false for the opponent. If we take a look at the `onLoad` method, we can use this to position it either at the top if it’s the opponent, or at the bottom if it’s the player. We can also see that we are adding a `CircleHitbox`, because we need to detect collisions between it and the bullets. Lastly, we are adding `_Gauge` as its child, which is the health point gauge you see on top of each players. Within `onCollision` callback, we are checking if the collided object is the opponent’s bullet, and if it is, we flag the bullet as `hasBeenHit` and remove it from the game.
|
||||
|
||||
`getMirroredPercentPosition` method is used when sharing the position with the opponent’s client. It calculates the mirrored position of the player. `updateHealth` is called when the health changes and updates the bar length of the `_Gauge` class.
|
||||
|
||||
### Adding bullets
|
||||
|
||||
Lastly we will add the `Bullet` class. It represents a single bullet coming out from the player and the opponent. Within `onLoad` it adds the sprite to apply the nice image and `CircleHitbox` so that it can collide with other objects. You can also see that it receives a `velocity` in the constructor, the position is updated using the velocity and the elapsed time within the `update` method. This is how you can have it move in a single direction at a constant speed.
|
||||
|
||||
```dart
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/image_composition.dart' as flame_image;
|
||||
|
||||
class Bullet extends PositionComponent with CollisionCallbacks, HasGameRef {
|
||||
final Vector2 velocity;
|
||||
|
||||
final flame_image.Image image;
|
||||
|
||||
static const radius = 5.0;
|
||||
|
||||
bool hasBeenHit = false;
|
||||
|
||||
final bool isMine;
|
||||
|
||||
/// Damage that it deals when it hits the player
|
||||
final int damage = 5;
|
||||
|
||||
Bullet({
|
||||
required this.isMine,
|
||||
required this.velocity,
|
||||
required this.image,
|
||||
required Vector2 initialPosition,
|
||||
}) : super(position: initialPosition);
|
||||
|
||||
@override
|
||||
Future<void>? onLoad() async {
|
||||
anchor = Anchor.center;
|
||||
|
||||
width = radius * 2;
|
||||
height = radius * 2;
|
||||
|
||||
add(CircleHitbox()
|
||||
..collisionType = CollisionType.passive
|
||||
..anchor = Anchor.center);
|
||||
|
||||
final sprite =
|
||||
SpriteComponent.fromImage(image, size: Vector2.all(radius * 2));
|
||||
|
||||
add(sprite);
|
||||
await super.onLoad();
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
position += velocity * dt;
|
||||
|
||||
if (position.y < 0 || position.y > gameRef.size.y) {
|
||||
removeFromParent();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4. Add real-time communications between players
|
||||
|
||||
At this point, we have a working shooting game except the opponent does not move, because we have not added any ways to communicate between clients. We will use Supabase’s [realtime features](https://supabase.com/docs/guides/realtime) for this, because it gives us an out of the box solution to handle low-latency real-time communication between players. If you do not have a Supabase project created yet, head over to [database.new](https://database.new/) to create one.
|
||||
|
||||
Before we get into any coding, let’s install the Supabase SDK into our app. We will also use the [uuid package](https://pub.dev/packages/uuid) to generate random unique ids for the users. Add the following to `pubspec.yaml` file.
|
||||
|
||||
```yaml
|
||||
supabase_flutter: ^1.4.0
|
||||
uuid: ^3.0.7
|
||||
```
|
||||
|
||||
Once `pub get` is complete, let’s initialize Supabase. We will override the `main` function to initialize Supabase. You can get your Supabase URL and Anon Key at `Project Setting` > `API`. Copy and paste them into the `Supabase.initialize` call.
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
await Supabase.initialize(
|
||||
url: 'YOUR_SUPABASE_URL',
|
||||
anonKey: 'YOUR_ANON_KEY',
|
||||
realtimeClientOptions: const RealtimeClientOptions(eventsPerSecond: 40),
|
||||
);
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
// Extract Supabase client for easy access to Supabase
|
||||
final supabase = Supabase.instance.client;
|
||||
```
|
||||
|
||||
`RealtimeClientOptions` here is a parameter to override how many events per second each client will send to Supabase. The default is 10, but we want to override to 40 to provide a more in-synced experience.
|
||||
|
||||
With this, we are ready to get into adding the real-time features now.
|
||||
|
||||
### Handle the Lobby to wait for Other Players to show up
|
||||
|
||||
We will start by rewriting the `_Lobby` class the first thing we have to do in the lobby is to wait and detect other online users also at the lobby. We can implement this using the [presence](https://supabase.com/docs/guides/realtime/presence) feature in Supabase.
|
||||
|
||||
Add `initState` and inside it initialize a `RealtimeChannel` instance. We can call it `_lobbyChannel`. If we take a look at the `subscribe()` method, we can see that upon successful subscription to the lobby channel, we are tracking our the unique user ID that we create uplon initialization. We are listening to the `sync` event to get notified whenever anyone is “present”. Within the callback, we are extracting the userIds of all the users in the lobby and set it as the state.A game starts when someone taps on the `Start` button. If we take a look at the `onPressed` callback, we see that we are sending a [broadcast](https://supabase.com/docs/guides/realtime/broadcast) event to the lobby channel with two participant ids and a randomly generated game ID. [Broadcast](https://supabase.com/docs/guides/realtime/broadcast) is a feature of Supabase to send and receive lightweight low-latency data between clients, and when the two participants, one of them being the person tapping on the `start` button, is received on both ends, a game starts. We can observe within `initState` inside the callback for `game_start` event that upon receiving a broadcast event, it checks if the player is one of the participants, and if it is, it will call the `onGameStarted` call back and pop the navigator removing the dialog. The game has begun!
|
||||
|
||||
```dart
|
||||
class _LobbyDialogState extends State<_LobbyDialog> {
|
||||
List<String> _userids = [];
|
||||
bool _loading = false;
|
||||
|
||||
/// Unique identifier for each players to identify eachother in lobby
|
||||
final myUserId = const Uuid().v4();
|
||||
|
||||
late final RealtimeChannel _lobbyChannel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_lobbyChannel = supabase.channel(
|
||||
'lobby',
|
||||
opts: const RealtimeChannelConfig(self: true),
|
||||
);
|
||||
_lobbyChannel.on(RealtimeListenTypes.presence, ChannelFilter(event: 'sync'),
|
||||
(payload, [ref]) {
|
||||
// Update the lobby count
|
||||
final presenceState = _lobbyChannel.presenceState();
|
||||
|
||||
setState(() {
|
||||
_userids = presenceState.values
|
||||
.map((presences) =>
|
||||
(presences.first as Presence).payload['user_id'] as String)
|
||||
.toList();
|
||||
});
|
||||
}).on(RealtimeListenTypes.broadcast, ChannelFilter(event: 'game_start'),
|
||||
(payload, [_]) {
|
||||
// Start the game if someone has started a game with you
|
||||
final participantIds = List<String>.from(payload['participants']);
|
||||
if (participantIds.contains(myUserId)) {
|
||||
final gameId = payload['game_id'] as String;
|
||||
widget.onGameStarted(gameId);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}).subscribe(
|
||||
(status, [ref]) async {
|
||||
if (status == 'SUBSCRIBED') {
|
||||
await _lobbyChannel.track({'user_id': myUserId});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
supabase.removeChannel(_lobbyChannel);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Lobby'),
|
||||
content: _loading
|
||||
? const SizedBox(
|
||||
height: 100,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
: Text('${_userids.length} users waiting'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _userids.length < 2
|
||||
? null
|
||||
: () async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
});
|
||||
|
||||
final opponentId =
|
||||
_userids.firstWhere((userId) => userId != myUserId);
|
||||
final gameId = const Uuid().v4();
|
||||
await _lobbyChannel.send(
|
||||
type: RealtimeListenTypes.broadcast,
|
||||
event: 'game_start',
|
||||
payload: {
|
||||
'participants': [
|
||||
opponentId,
|
||||
myUserId,
|
||||
],
|
||||
'game_id': gameId,
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text('start'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sharing Game States with the Opposing Player
|
||||
|
||||
Once a game begins, we need to synchronize the game states between the two clients. In our case, we will sync only the player’s position and health points. Whenever a player moves, or the player’s health points change, the `onGameStateUpdate` callback on our `MyGame` instance that will fire notifying the update along with its position and health point. We will broadcast those information to the opponent’s client via Supabase [broadcast feature](https://supabase.com/docs/guides/realtime/broadcast).
|
||||
|
||||
Fill in the `_initialize` method like the following to initialize the game.
|
||||
|
||||
```dart
|
||||
class GamePage extends StatefulWidget {
|
||||
const GamePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<GamePage> createState() => _GamePageState();
|
||||
}
|
||||
|
||||
class _GamePageState extends State<GamePage> {
|
||||
late final MyGame _game;
|
||||
|
||||
/// Holds the RealtimeChannel to sync game states
|
||||
RealtimeChannel? _gameChannel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Image.asset('assets/images/background.jpg', fit: BoxFit.cover),
|
||||
GameWidget(game: _game),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initialize();
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
_game = MyGame(
|
||||
onGameStateUpdate: (position, health) async {
|
||||
ChannelResponse response;
|
||||
// Loop until the send succeeds if the payload is to notify defeat.
|
||||
do {
|
||||
response = await _gameChannel!.send(
|
||||
type: RealtimeListenTypes.broadcast,
|
||||
event: 'game_state',
|
||||
payload: {'x': position.x, 'y': position.y, 'health': health},
|
||||
);
|
||||
|
||||
// wait for a frame to avoid infinite rate limiting loops
|
||||
await Future.delayed(Duration.zero);
|
||||
setState(() {});
|
||||
} while (response == ChannelResponse.rateLimited && health <= 0);
|
||||
},
|
||||
onGameOver: (playerWon) async {
|
||||
await showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: ((context) {
|
||||
return AlertDialog(
|
||||
title: Text(playerWon ? 'You Won!' : 'You lost...'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
await supabase.removeChannel(_gameChannel!);
|
||||
_openLobbyDialog();
|
||||
},
|
||||
child: const Text('Back to Lobby'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// await for a frame so that the widget mounts
|
||||
await Future.delayed(Duration.zero);
|
||||
|
||||
if (mounted) {
|
||||
_openLobbyDialog();
|
||||
}
|
||||
}
|
||||
|
||||
void _openLobbyDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
return _LobbyDialog(
|
||||
onGameStarted: (gameId) async {
|
||||
// await a frame to allow subscribing to a new channel in a realtime callback
|
||||
await Future.delayed(Duration.zero);
|
||||
|
||||
setState(() {});
|
||||
|
||||
_game.startNewGame();
|
||||
|
||||
_gameChannel = supabase.channel(gameId,
|
||||
opts: const RealtimeChannelConfig(ack: true));
|
||||
|
||||
_gameChannel!.on(RealtimeListenTypes.broadcast,
|
||||
ChannelFilter(event: 'game_state'), (payload, [_]) {
|
||||
final position =
|
||||
Vector2(payload['x'] as double, payload['y'] as double);
|
||||
final opponentHealth = payload['health'] as int;
|
||||
_game.updateOpponent(
|
||||
position: position,
|
||||
health: opponentHealth,
|
||||
);
|
||||
|
||||
if (opponentHealth <= 0) {
|
||||
if (!_game.isGameOver) {
|
||||
_game.isGameOver = true;
|
||||
_game.onGameOver(true);
|
||||
}
|
||||
}
|
||||
}).subscribe();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can see that within `_openLobbyDialog`, there is an `onGameStarted` callback for when the game has started. Once a game has been started, it creates a new channel using the game ID as the channel name and starts listening to game state updates from the opponent.You can see that within the `onGameOver` callback, we are showing a simple dialog. Upon tapping `Back to Lobby`, the user will be taken back to the lobby dialog, where they can start another game if they want to.
|
||||
|
||||

|
||||
|
||||
With all of that put together, we have a functioning real-time multiplayer shooting game. Grab a friend, run the app with `flutter run`, and have fun with it!
|
||||
|
||||
## Conclusions
|
||||
|
||||
We learned how to create an interactive shooting game. We took advantage of Flutter’s dialogs to create a quick and easy lobby and post-game UI. Then we created the game using Flame. We learned how to detect and handle collisions and experienced how easy creating a sophisticated game was using Flame. Finally, we added capabilities to share game states with other clients to complete a real-time multiplayer experience without managing our own infrastructure using [Supabase](https://supabase.com/).
|
||||
|
||||
## More Flutter Resources
|
||||
|
||||
- [Complete code of this article](https://github.com/supabase/supabase/tree/master/examples/realtime/examples/flutter-multiplayer-shooting-game)
|
||||
- [Flame Engine website](https://flame-engine.org/)
|
||||
- [Generate Flame template using Very Good CLI](https://verygood.ventures/blog/generate-a-game-with-our-new-template)
|
||||
- [Supabase Flutter SDK docs](https://supabase.com/docs/reference/dart/start)
|
||||
@@ -149,7 +149,7 @@
|
||||
"author": "Tyler Shukert",
|
||||
"username": "dshukertjr",
|
||||
"position": "DevRel & Flutter",
|
||||
"author_url": "https://github.com/dshukertjr",
|
||||
"author_url": "https://twitter.com/dshukertjr",
|
||||
"author_image_url": "https://github.com/dshukertjr.png"
|
||||
},
|
||||
{
|
||||
|
||||
|
After Width: | Height: | Size: 869 KiB |
|
After Width: | Height: | Size: 404 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,46 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
/pubspec.lock
|
||||
@@ -0,0 +1,48 @@
|
||||
# Flutter Real-time Multiplayer Shooting Game
|
||||
|
||||
A real-time shooting game built with [Flutter](https://flutter.dev/), [Flame](https://flame-engine.org/) and [Supabase](https://supabase.com).
|
||||
|
||||
You can find a step by step guide on how to build this app on [How to build a real-time multiplayer game with Flutter Flame](https://supabase.com/blog/flutter-real-time-multiplayer-game) article.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Create new project
|
||||
|
||||
Sign up to Supabase - [app.supabase.io](https://app.supabase.io) and create a new project. Wait for your database to start.
|
||||
|
||||
### 2. Get the URL and Key
|
||||
|
||||
Go to the Project Settings (the cog icon), open the API tab, and find your API URL and `anon` key, you'll need these in the next step.
|
||||
|
||||
The `anon` key is your client-side API key. It allows "anonymous access" to your database, until the user has logged in. Once they have logged in, the keys will switch to the user's own login token.
|
||||
|
||||

|
||||
|
||||
### 3. Pull this example git repository
|
||||
|
||||
`git clone <<this repository url>> `
|
||||
|
||||
### 4. Paste the Supabase URL and Anon Key
|
||||
|
||||
Copy and paste the Supabase URL and Anon key in `lib/main.dart` file
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
await Supabase.initialize(
|
||||
url: 'supabaseUrl',
|
||||
anonKey: 'supabaseAnonKey',
|
||||
realtimeClientOptions: const RealtimeClientOptions(eventsPerSecond: 40),
|
||||
);
|
||||
runApp(const MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Run the Flutter App
|
||||
|
||||
Now run
|
||||
|
||||
```bash
|
||||
flutter run
|
||||
```
|
||||
|
||||
This app can run on any environment that runs Flutter.
|
||||
@@ -0,0 +1,29 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at
|
||||
# https://dart-lang.github.io/linter/lints/index.html.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
@@ -0,0 +1,13 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
@@ -0,0 +1,71 @@
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.example.flame_realtime_shooting"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.flame_realtime_shooting">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,34 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.flame_realtime_shooting">
|
||||
<application
|
||||
android:label="flame_realtime_shooting"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.example.flame_realtime_shooting
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.flame_realtime_shooting">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,31 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||
@@ -0,0 +1,11 @@
|
||||
include ':app'
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
|
After Width: | Height: | Size: 586 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
@@ -0,0 +1,34 @@
|
||||
**/dgph
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/ephemeral/
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>11.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
@@ -0,0 +1,41 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '11.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_ios_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
PODS:
|
||||
- app_links (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_links:
|
||||
:path: ".symlinks/plugins/app_links/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: ab4ba54d10a13d45825336bc9707b5eadee81191
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
|
||||
url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
|
||||
|
||||
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
@@ -0,0 +1,551 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
BE23484369E0A627E72B448C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B449CD597D82C8759DF3E2B7 /* Pods_Runner.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
704E3DD2DB644C18A6649B8B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
A5EAF25F523BC10830465093 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
B449CD597D82C8759DF3E2B7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C4C86A4EFA7674C82FEAA392 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BE23484369E0A627E72B448C /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
27852139F92CD9F990CE4A71 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B449CD597D82C8759DF3E2B7 /* Pods_Runner.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
35EBDD9546014B9099FCEC6A /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C4C86A4EFA7674C82FEAA392 /* Pods-Runner.debug.xcconfig */,
|
||||
A5EAF25F523BC10830465093 /* Pods-Runner.release.xcconfig */,
|
||||
704E3DD2DB644C18A6649B8B /* Pods-Runner.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
35EBDD9546014B9099FCEC6A /* Pods */,
|
||||
27852139F92CD9F990CE4A71 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
547CCC55113E27B88A271312 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
80DF38846FB2DB612DDD07ED /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1300;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
547CCC55113E27B88A271312 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
80DF38846FB2DB612DDD07ED /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flameRealtimeShooting;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flameRealtimeShooting;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.flameRealtimeShooting;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147031CF9000F007C117D /* Debug */,
|
||||
97C147041CF9000F007C117D /* Release */,
|
||||
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147061CF9000F007C117D /* Debug */,
|
||||
97C147071CF9000F007C117D /* Release */,
|
||||
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,13 @@
|
||||
import UIKit
|
||||
import Flutter
|
||||
|
||||
@UIApplicationMain
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"size": "20x20",
|
||||
"idiom": "iphone",
|
||||
"filename": "Icon-App-20x20@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size": "20x20",
|
||||
"idiom": "iphone",
|
||||
"filename": "Icon-App-20x20@3x.png",
|
||||
"scale": "3x"
|
||||
},
|
||||
{
|
||||
"size": "29x29",
|
||||
"idiom": "iphone",
|
||||
"filename": "Icon-App-29x29@1x.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size": "29x29",
|
||||
"idiom": "iphone",
|
||||
"filename": "Icon-App-29x29@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size": "29x29",
|
||||
"idiom": "iphone",
|
||||
"filename": "Icon-App-29x29@3x.png",
|
||||
"scale": "3x"
|
||||
},
|
||||
{
|
||||
"size": "40x40",
|
||||
"idiom": "iphone",
|
||||
"filename": "Icon-App-40x40@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size": "40x40",
|
||||
"idiom": "iphone",
|
||||
"filename": "Icon-App-40x40@3x.png",
|
||||
"scale": "3x"
|
||||
},
|
||||
{
|
||||
"size": "60x60",
|
||||
"idiom": "iphone",
|
||||
"filename": "Icon-App-60x60@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size": "60x60",
|
||||
"idiom": "iphone",
|
||||
"filename": "Icon-App-60x60@3x.png",
|
||||
"scale": "3x"
|
||||
},
|
||||
{
|
||||
"size": "20x20",
|
||||
"idiom": "ipad",
|
||||
"filename": "Icon-App-20x20@1x.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size": "20x20",
|
||||
"idiom": "ipad",
|
||||
"filename": "Icon-App-20x20@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size": "29x29",
|
||||
"idiom": "ipad",
|
||||
"filename": "Icon-App-29x29@1x.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size": "29x29",
|
||||
"idiom": "ipad",
|
||||
"filename": "Icon-App-29x29@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size": "40x40",
|
||||
"idiom": "ipad",
|
||||
"filename": "Icon-App-40x40@1x.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size": "40x40",
|
||||
"idiom": "ipad",
|
||||
"filename": "Icon-App-40x40@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size": "76x76",
|
||||
"idiom": "ipad",
|
||||
"filename": "Icon-App-76x76@1x.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size": "76x76",
|
||||
"idiom": "ipad",
|
||||
"filename": "Icon-App-76x76@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size": "83.5x83.5",
|
||||
"idiom": "ipad",
|
||||
"filename": "Icon-App-83.5x83.5@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size": "1024x1024",
|
||||
"idiom": "ios-marketing",
|
||||
"filename": "Icon-App-1024x1024@1x.png",
|
||||
"scale": "1x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "LaunchImage.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "LaunchImage@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "LaunchImage@3x.png",
|
||||
"scale": "3x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
@@ -0,0 +1,5 @@
|
||||
# Launch Screen Assets
|
||||
|
||||
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||
|
||||
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Flame Realtime Shooting</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>flame_realtime_shooting</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/image_composition.dart' as flame_image;
|
||||
|
||||
class Bullet extends PositionComponent with CollisionCallbacks, HasGameRef {
|
||||
final Vector2 velocity;
|
||||
|
||||
final flame_image.Image image;
|
||||
|
||||
static const radius = 5.0;
|
||||
|
||||
bool hasBeenHit = false;
|
||||
|
||||
final bool isMine;
|
||||
|
||||
/// Damage that it deals when it hits the player
|
||||
final int damage = 5;
|
||||
|
||||
Bullet({
|
||||
required this.isMine,
|
||||
required this.velocity,
|
||||
required this.image,
|
||||
required Vector2 initialPosition,
|
||||
}) : super(position: initialPosition);
|
||||
|
||||
@override
|
||||
Future<void>? onLoad() async {
|
||||
anchor = Anchor.center;
|
||||
|
||||
width = radius * 2;
|
||||
height = radius * 2;
|
||||
|
||||
add(CircleHitbox()
|
||||
..collisionType = CollisionType.passive
|
||||
..anchor = Anchor.center);
|
||||
|
||||
final sprite =
|
||||
SpriteComponent.fromImage(image, size: Vector2.all(radius * 2));
|
||||
|
||||
add(sprite);
|
||||
await super.onLoad();
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
position += velocity * dt;
|
||||
|
||||
if (position.y < 0 || position.y > gameRef.size.y) {
|
||||
removeFromParent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/events.dart';
|
||||
import 'package:flame/image_composition.dart' as flame_image;
|
||||
import 'package:flame_realtime_shooting/game/bullet.dart';
|
||||
import 'package:flame_realtime_shooting/game/player.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MyGame extends FlameGame with PanDetector, HasCollisionDetection {
|
||||
MyGame({
|
||||
required this.onGameOver,
|
||||
required this.onGameStateUpdate,
|
||||
});
|
||||
|
||||
static const _initialHealthPoints = 100;
|
||||
|
||||
/// Callback to notify the parent when the game ends.
|
||||
final void Function(bool didWin) onGameOver;
|
||||
|
||||
/// Callback for when the game state updates.
|
||||
final void Function(
|
||||
Vector2 position,
|
||||
int health,
|
||||
) onGameStateUpdate;
|
||||
|
||||
/// `Player` instance of the player
|
||||
late Player _player;
|
||||
|
||||
/// `Player` instance of the opponent
|
||||
late Player _opponent;
|
||||
|
||||
bool isGameOver = true;
|
||||
|
||||
int _playerHealthPoint = _initialHealthPoints;
|
||||
|
||||
late final flame_image.Image _playerBulletImage;
|
||||
late final flame_image.Image _opponentBulletImage;
|
||||
|
||||
@override
|
||||
Color backgroundColor() {
|
||||
return Colors.transparent;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void>? onLoad() async {
|
||||
final playerImage = await images.load('player.png');
|
||||
_player = Player(isMe: true);
|
||||
final spriteSize = Vector2.all(Player.radius * 2);
|
||||
_player.add(SpriteComponent(sprite: Sprite(playerImage), size: spriteSize));
|
||||
add(_player);
|
||||
|
||||
final opponentImage = await images.load('opponent.png');
|
||||
_opponent = Player(isMe: false);
|
||||
_opponent.add(SpriteComponent.fromImage(opponentImage, size: spriteSize));
|
||||
add(_opponent);
|
||||
|
||||
_playerBulletImage = await images.load('player-bullet.png');
|
||||
_opponentBulletImage = await images.load('opponent-bullet.png');
|
||||
|
||||
await super.onLoad();
|
||||
}
|
||||
|
||||
@override
|
||||
void onPanUpdate(DragUpdateInfo info) {
|
||||
_player.move(info.delta.global);
|
||||
final mirroredPosition = _player.getMirroredPercentPosition();
|
||||
onGameStateUpdate(mirroredPosition, _playerHealthPoint);
|
||||
super.onPanUpdate(info);
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
if (isGameOver) {
|
||||
return;
|
||||
}
|
||||
for (final child in children) {
|
||||
if (child is Bullet && child.hasBeenHit && !child.isMine) {
|
||||
_playerHealthPoint = _playerHealthPoint - child.damage;
|
||||
final mirroredPosition = _player.getMirroredPercentPosition();
|
||||
onGameStateUpdate(mirroredPosition, _playerHealthPoint);
|
||||
_player.updateHealth(_playerHealthPoint / _initialHealthPoints);
|
||||
}
|
||||
}
|
||||
if (_playerHealthPoint <= 0) {
|
||||
endGame(false);
|
||||
}
|
||||
}
|
||||
|
||||
void startNewGame() {
|
||||
isGameOver = false;
|
||||
_playerHealthPoint = _initialHealthPoints;
|
||||
|
||||
for (final child in children) {
|
||||
if (child is Player) {
|
||||
child.position = child.initialPosition;
|
||||
} else if (child is Bullet) {
|
||||
child.removeFromParent();
|
||||
}
|
||||
}
|
||||
|
||||
_shootBullets();
|
||||
}
|
||||
|
||||
/// shoots out bullets form both the player and the opponent.
|
||||
///
|
||||
/// Calls itself every 500 milliseconds
|
||||
Future<void> _shootBullets() async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
/// Player's bullet
|
||||
final playerBulletInitialPosition = Vector2.copy(_player.position)
|
||||
..y -= Player.radius;
|
||||
final playerBulletVelocities = [
|
||||
Vector2(0, -100),
|
||||
Vector2(60, -80),
|
||||
Vector2(-60, -80),
|
||||
];
|
||||
for (final bulletVelocity in playerBulletVelocities) {
|
||||
add((Bullet(
|
||||
isMine: true,
|
||||
velocity: bulletVelocity,
|
||||
image: _playerBulletImage,
|
||||
initialPosition: playerBulletInitialPosition,
|
||||
)));
|
||||
}
|
||||
|
||||
/// Opponent's bullet
|
||||
final opponentBulletInitialPosition = Vector2.copy(_opponent.position)
|
||||
..y += Player.radius;
|
||||
final opponentBulletVelocities = [
|
||||
Vector2(0, 100),
|
||||
Vector2(60, 80),
|
||||
Vector2(-60, 80),
|
||||
];
|
||||
for (final bulletVelocity in opponentBulletVelocities) {
|
||||
add((Bullet(
|
||||
isMine: false,
|
||||
velocity: bulletVelocity,
|
||||
image: _opponentBulletImage,
|
||||
initialPosition: opponentBulletInitialPosition,
|
||||
)));
|
||||
}
|
||||
|
||||
_shootBullets();
|
||||
}
|
||||
|
||||
void updateOpponent({required Vector2 position, required int health}) {
|
||||
_opponent.position = Vector2(size.x * position.x, size.y * position.y);
|
||||
_opponent.updateHealth(health / _initialHealthPoints);
|
||||
}
|
||||
|
||||
/// Called when either the player or the opponent has run out of health points
|
||||
void endGame(bool playerWon) {
|
||||
isGameOver = true;
|
||||
onGameOver(playerWon);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_realtime_shooting/game/bullet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Player extends PositionComponent with HasGameRef, CollisionCallbacks {
|
||||
Vector2 velocity = Vector2.zero();
|
||||
|
||||
late final Vector2 initialPosition;
|
||||
|
||||
Player({required bool isMe}) : _isMyPlayer = isMe;
|
||||
|
||||
/// Whether it's me or the opponent
|
||||
final bool _isMyPlayer;
|
||||
|
||||
static const radius = 30.0;
|
||||
|
||||
@override
|
||||
Future<void>? onLoad() async {
|
||||
anchor = Anchor.center;
|
||||
width = radius * 2;
|
||||
height = radius * 2;
|
||||
|
||||
final initialX = gameRef.size.x / 2;
|
||||
initialPosition = _isMyPlayer
|
||||
? Vector2(initialX, gameRef.size.y * 0.8)
|
||||
: Vector2(initialX, gameRef.size.y * 0.2);
|
||||
position = initialPosition;
|
||||
|
||||
add(CircleHitbox());
|
||||
add(_Gauge());
|
||||
await super.onLoad();
|
||||
}
|
||||
|
||||
void move(Vector2 delta) {
|
||||
position += delta;
|
||||
}
|
||||
|
||||
void updateHealth(double healthLeft) {
|
||||
for (final child in children) {
|
||||
if (child is _Gauge) {
|
||||
child._healthLeft = healthLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
|
||||
super.onCollision(intersectionPoints, other);
|
||||
if (other is Bullet && _isMyPlayer != other.isMine) {
|
||||
other.hasBeenHit = true;
|
||||
other.removeFromParent();
|
||||
}
|
||||
}
|
||||
|
||||
/// returns the mirrored percent position of the player
|
||||
/// to be broadcasted to other clients
|
||||
Vector2 getMirroredPercentPosition() {
|
||||
final mirroredPosition = gameRef.size - position;
|
||||
return Vector2(mirroredPosition.x / gameRef.size.x,
|
||||
mirroredPosition.y / gameRef.size.y);
|
||||
}
|
||||
}
|
||||
|
||||
class _Gauge extends PositionComponent {
|
||||
double _healthLeft = 1.0;
|
||||
|
||||
@override
|
||||
FutureOr<void> onLoad() {
|
||||
final playerParent = parent;
|
||||
if (playerParent is Player) {
|
||||
width = playerParent.width;
|
||||
height = 10;
|
||||
anchor = Anchor.centerLeft;
|
||||
position = Vector2(0, 0);
|
||||
}
|
||||
return super.onLoad();
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
super.render(canvas);
|
||||
canvas.drawRect(
|
||||
Rect.fromPoints(
|
||||
const Offset(0, 0),
|
||||
Offset(width, height),
|
||||
),
|
||||
Paint()..color = Colors.white);
|
||||
canvas.drawRect(
|
||||
Rect.fromPoints(
|
||||
const Offset(0, 0),
|
||||
Offset(width * _healthLeft, height),
|
||||
),
|
||||
Paint()
|
||||
..color = _healthLeft > 0.5
|
||||
? Colors.green
|
||||
: _healthLeft > 0.25
|
||||
? Colors.orange
|
||||
: Colors.red);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_realtime_shooting/game/game.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
void main() async {
|
||||
await Supabase.initialize(
|
||||
url: 'supabaseUrl',
|
||||
anonKey: 'supabaseAnonKey',
|
||||
realtimeClientOptions: const RealtimeClientOptions(eventsPerSecond: 40),
|
||||
);
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
title: 'UFO Shooting Game',
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: GamePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GamePage extends StatefulWidget {
|
||||
const GamePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<GamePage> createState() => _GamePageState();
|
||||
}
|
||||
|
||||
class _GamePageState extends State<GamePage> {
|
||||
late final MyGame _game;
|
||||
|
||||
/// Holds the RealtimeChannel to sync game states
|
||||
RealtimeChannel? _gameChannel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Image.asset('assets/images/background.jpg', fit: BoxFit.cover),
|
||||
GameWidget(game: _game),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initialize();
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
_game = MyGame(
|
||||
onGameStateUpdate: (position, health) async {
|
||||
ChannelResponse response;
|
||||
do {
|
||||
response = await _gameChannel!.send(
|
||||
type: RealtimeListenTypes.broadcast,
|
||||
event: 'game_state',
|
||||
payload: {'x': position.x, 'y': position.y, 'health': health},
|
||||
);
|
||||
|
||||
// wait for a frame to avoid infinite rate limiting loops
|
||||
await Future.delayed(Duration.zero);
|
||||
setState(() {});
|
||||
} while (response == ChannelResponse.rateLimited && health <= 0);
|
||||
},
|
||||
onGameOver: (playerWon) async {
|
||||
await showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: ((context) {
|
||||
return AlertDialog(
|
||||
title: Text(playerWon ? 'You Won!' : 'You lost...'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
await supabase.removeChannel(_gameChannel!);
|
||||
_openLobbyDialog();
|
||||
},
|
||||
child: const Text('Back to Lobby'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// await for a frame so that the widget mounts
|
||||
await Future.delayed(Duration.zero);
|
||||
|
||||
if (mounted) {
|
||||
_openLobbyDialog();
|
||||
}
|
||||
}
|
||||
|
||||
void _openLobbyDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
return _LobbyDialog(
|
||||
onGameStarted: (gameId) async {
|
||||
// await a frame to allow subscribing to a new channel in a realtime callback
|
||||
await Future.delayed(Duration.zero);
|
||||
|
||||
setState(() {});
|
||||
|
||||
_game.startNewGame();
|
||||
|
||||
_gameChannel = supabase.channel(gameId,
|
||||
opts: const RealtimeChannelConfig(ack: true));
|
||||
|
||||
_gameChannel!.on(RealtimeListenTypes.broadcast,
|
||||
ChannelFilter(event: 'game_state'), (payload, [_]) {
|
||||
final position =
|
||||
Vector2(payload['x'] as double, payload['y'] as double);
|
||||
final opponentHealth = payload['health'] as int;
|
||||
_game.updateOpponent(
|
||||
position: position,
|
||||
health: opponentHealth,
|
||||
);
|
||||
|
||||
if (opponentHealth <= 0) {
|
||||
if (!_game.isGameOver) {
|
||||
_game.isGameOver = true;
|
||||
_game.onGameOver(true);
|
||||
}
|
||||
}
|
||||
}).subscribe();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _LobbyDialog extends StatefulWidget {
|
||||
const _LobbyDialog({
|
||||
required this.onGameStarted,
|
||||
});
|
||||
|
||||
final void Function(String gameId) onGameStarted;
|
||||
|
||||
@override
|
||||
State<_LobbyDialog> createState() => _LobbyDialogState();
|
||||
}
|
||||
|
||||
class _LobbyDialogState extends State<_LobbyDialog> {
|
||||
List<String> _userids = [];
|
||||
bool _loading = false;
|
||||
|
||||
/// Unique identifier for each players to identify eachother in lobby
|
||||
final myUserId = const Uuid().v4();
|
||||
|
||||
late final RealtimeChannel _lobbyChannel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_lobbyChannel = supabase.channel(
|
||||
'lobby',
|
||||
opts: const RealtimeChannelConfig(self: true),
|
||||
);
|
||||
_lobbyChannel.on(RealtimeListenTypes.presence, ChannelFilter(event: 'sync'),
|
||||
(payload, [ref]) {
|
||||
// Update the lobby count
|
||||
final presenceState = _lobbyChannel.presenceState();
|
||||
|
||||
setState(() {
|
||||
_userids = presenceState.values
|
||||
.map((presences) =>
|
||||
(presences.first as Presence).payload['user_id'] as String)
|
||||
.toList();
|
||||
});
|
||||
}).on(RealtimeListenTypes.broadcast, ChannelFilter(event: 'game_start'),
|
||||
(payload, [_]) {
|
||||
// Start the game if someone has started a game with you
|
||||
final participantIds = List<String>.from(payload['participants']);
|
||||
if (participantIds.contains(myUserId)) {
|
||||
final gameId = payload['game_id'] as String;
|
||||
widget.onGameStarted(gameId);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}).subscribe(
|
||||
(status, [ref]) async {
|
||||
if (status == 'SUBSCRIBED') {
|
||||
await _lobbyChannel.track({'user_id': myUserId});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
supabase.removeChannel(_lobbyChannel);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Lobby'),
|
||||
content: _loading
|
||||
? const SizedBox(
|
||||
height: 100,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
: Text('${_userids.length} users waiting'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _userids.length < 2
|
||||
? null
|
||||
: () async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
});
|
||||
|
||||
final opponentId =
|
||||
_userids.firstWhere((userId) => userId != myUserId);
|
||||
final gameId = const Uuid().v4();
|
||||
await _lobbyChannel.send(
|
||||
type: RealtimeListenTypes.broadcast,
|
||||
event: 'game_start',
|
||||
payload: {
|
||||
'participants': [
|
||||
opponentId,
|
||||
myUserId,
|
||||
],
|
||||
'game_id': gameId,
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text('start'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
name: flame_realtime_shooting
|
||||
description: Multiplayer shooting game using Flutter, Flame and Supabase.
|
||||
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: '>=2.19.0 <3.0.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
supabase_flutter: ^1.4.0
|
||||
flame: ^1.6.0
|
||||
uuid: ^3.0.7
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
flutter_lints: ^2.0.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- assets/images/
|
||||
@@ -0,0 +1,30 @@
|
||||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:flame_realtime_shooting/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsNothing);
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
After Width: | Height: | Size: 917 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
|
||||
The path provided below has to start and end with a slash "/" in order for
|
||||
it to work correctly.
|
||||
|
||||
For more details:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||
|
||||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="flame_realtime_shooting">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>flame_realtime_shooting</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
||||
<script>
|
||||
// The value below is injected by flutter build, do not touch.
|
||||
var serviceWorkerVersion = null;
|
||||
</script>
|
||||
<!-- This script adds the flutter initialization JS code -->
|
||||
<script src="flutter.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.addEventListener('load', function(ev) {
|
||||
// Download main.dart.js
|
||||
_flutter.loader.loadEntrypoint({
|
||||
serviceWorker: {
|
||||
serviceWorkerVersion: serviceWorkerVersion,
|
||||
},
|
||||
onEntrypointLoaded: function(engineInitializer) {
|
||||
engineInitializer.initializeEngine().then(function(appRunner) {
|
||||
appRunner.runApp();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "flame_realtime_shooting",
|
||||
"short_name": "flame_realtime_shooting",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A new Flutter project.",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -62,13 +62,11 @@ const SSLConfiguration = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAccessToSSLEnforcement) return <></>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<FormHeader title="SSL Configuration" description="" />
|
||||
<div className="flex items-center space-x-2 mb-6">
|
||||
{/* <div className="flex items-center space-x-2 mb-6">
|
||||
<Link href="https://supabase.com/docs/guides/platform/ssl-enforcement">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
@@ -76,53 +74,55 @@ const SSLConfiguration = () => {
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
<FormPanel>
|
||||
<FormSection
|
||||
header={
|
||||
<FormSectionLabel
|
||||
className="lg:col-span-7"
|
||||
description={
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-scale-1000">
|
||||
Reject non-SSL connections to your database
|
||||
</p>
|
||||
{!sslEnforcementConfiguration?.appliedSuccessfully && (
|
||||
<Alert
|
||||
withIcon
|
||||
variant="warning"
|
||||
title="SSL enforcement was not updated successfully"
|
||||
>
|
||||
Please try updating again, or contact{' '}
|
||||
<Link href="/support/new">
|
||||
<a target="_blank" className="underline">
|
||||
support
|
||||
</a>
|
||||
</Link>{' '}
|
||||
if this error persists
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Enforce SSL on incoming connections
|
||||
</FormSectionLabel>
|
||||
}
|
||||
>
|
||||
<FormSectionContent loading={false} className="lg:!col-span-5">
|
||||
<div className="flex items-center justify-end mt-2.5 space-x-2">
|
||||
{(isLoading || isSubmitting) && (
|
||||
<IconLoader className="animate-spin" strokeWidth={1.5} size={16} />
|
||||
)}
|
||||
<Toggle
|
||||
checked={isEnforced}
|
||||
disabled={isLoading || isSubmitting}
|
||||
onChange={toggleSSLEnforcement}
|
||||
/>
|
||||
</div>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
{hasAccessToSSLEnforcement && (
|
||||
<FormSection
|
||||
header={
|
||||
<FormSectionLabel
|
||||
className="lg:col-span-7"
|
||||
description={
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-scale-1000">
|
||||
Reject non-SSL connections to your database
|
||||
</p>
|
||||
{!sslEnforcementConfiguration?.appliedSuccessfully && (
|
||||
<Alert
|
||||
withIcon
|
||||
variant="warning"
|
||||
title="SSL enforcement was not updated successfully"
|
||||
>
|
||||
Please try updating again, or contact{' '}
|
||||
<Link href="/support/new">
|
||||
<a target="_blank" className="underline">
|
||||
support
|
||||
</a>
|
||||
</Link>{' '}
|
||||
if this error persists
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Enforce SSL on incoming connections
|
||||
</FormSectionLabel>
|
||||
}
|
||||
>
|
||||
<FormSectionContent loading={false} className="lg:!col-span-5">
|
||||
<div className="flex items-center justify-end mt-2.5 space-x-2">
|
||||
{(isLoading || isSubmitting) && (
|
||||
<IconLoader className="animate-spin" strokeWidth={1.5} size={16} />
|
||||
)}
|
||||
<Toggle
|
||||
checked={isEnforced}
|
||||
disabled={isLoading || isSubmitting}
|
||||
onChange={toggleSSLEnforcement}
|
||||
/>
|
||||
</div>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 items-center lg:grid-cols-2 p-8">
|
||||
<div className="space-y-2">
|
||||
|
||||