unity_kit 2.0.0 copy "unity_kit: ^2.0.0" to clipboard
unity_kit: ^2.0.0 copied to clipboard

Flutter plugin for Unity 3D integration — typed bridge, binary protocol, performance + AR, on Android, iOS, web & desktop.

example/lib/main.dart

import 'dart:async';
import 'dart:developer' as developer;

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:unity_kit/unity_kit.dart';

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const String _kDefaultScene = 'MainScene';

// Parameter names sent to Unity
const String _kParamRotationX = 'rotationX';
const String _kParamRotationY = 'rotationY';
const String _kParamScale = 'scale';
const String _kParamSpeed = 'speed';
const String _kParamIntensity = 'intensity';
const String _kParamPositionX = 'positionX';
const String _kParamPositionY = 'positionY';

// Gesture sensitivity multipliers
const double _kRotationSensitivity = 0.5;
const double _kPositionSensitivity = 0.01;
const double _kScaleMin = 0.1;
const double _kScaleMax = 5.0;

// ---------------------------------------------------------------------------
// App entry point
// ---------------------------------------------------------------------------

void main() {
  runApp(const UnityKitExampleApp());
}

class UnityKitExampleApp extends StatelessWidget {
  const UnityKitExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Unity Kit Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: Colors.deepPurple,
        useMaterial3: true,
        brightness: Brightness.dark,
      ),
      home: const MainScreen(),
    );
  }
}

// ---------------------------------------------------------------------------
// Main screen -- Unity view + sliders
// ---------------------------------------------------------------------------

class MainScreen extends StatefulWidget {
  const MainScreen({super.key});

  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  bool _unityReady = false;
  UnityBridge? _bridge;

  // Feature toggles (2.0.0)
  UnityArMode _arMode = UnityArMode.none;
  bool _useBinary = false;
  UnityPerformanceStats? _perf;
  StreamSubscription<UnityPerformanceStats>? _perfSub;

  // Gesture-controlled values
  double _rotationX = 0;
  double _rotationY = 0;
  double _positionX = 0;
  double _positionY = 0;
  double _scale = 1.0;
  double _baseScale = 1.0;

  // Slider-controlled values
  double _speed = 1.0;
  double _intensity = 0.5;

  @override
  void dispose() {
    _perfSub?.cancel();
    super.dispose();
  }

  /// Sends [message] over the binary protocol or JSON depending on the toggle.
  void _send(UnityMessage message) {
    final bridge = _bridge;
    if (bridge == null) return;
    if (_useBinary && _unityReady) {
      bridge.sendBinary(message);
    } else {
      bridge.sendWhenReady(message);
    }
  }

  void _onArModeSelected(UnityArMode mode) {
    if (mode == _arMode) return;
    setState(() {
      _arMode = mode;
      // Recreating the platform view resets readiness.
      _unityReady = false;
      _bridge = null;
      _perf = null;
    });
  }

  // ---------------------------------------------------------------------------
  // Gesture handlers
  // ---------------------------------------------------------------------------

  void _onScaleStart(ScaleStartDetails details) {
    _baseScale = _scale;
  }

  void _onScaleUpdate(ScaleUpdateDetails details) {
    if (!_unityReady) return;

    setState(() {
      if (details.pointerCount == 1) {
        // Single finger drag = rotation
        _rotationY += details.focalPointDelta.dx * _kRotationSensitivity;
        _rotationX -= details.focalPointDelta.dy * _kRotationSensitivity;
      } else if (details.pointerCount >= 2) {
        // Two fingers: pinch = scale, pan = position
        _scale = (_baseScale * details.scale).clamp(_kScaleMin, _kScaleMax);
        _positionX += details.focalPointDelta.dx * _kPositionSensitivity;
        _positionY -= details.focalPointDelta.dy * _kPositionSensitivity;
      }
    });

    _sendTransform();
  }

  void _sendTransform() {
    if (_bridge == null) return;

    final params = {
      _kParamRotationX: _rotationX,
      _kParamRotationY: _rotationY,
      _kParamScale: _scale,
      _kParamPositionX: _positionX,
      _kParamPositionY: _positionY,
    };

    for (final entry in params.entries) {
      _send(
        UnityMessage.command('SetParameter', {
          'param': entry.key,
          'value': entry.value,
        }),
      );
    }
  }

  // ---------------------------------------------------------------------------
  // Slider change handlers
  // ---------------------------------------------------------------------------

  void _onSliderChanged(String param, double value) {
    _send(
      UnityMessage.command('SetParameter', {
        'param': param,
        'value': value,
      }),
    );
  }

  void _onSpeedChanged(double value) {
    setState(() => _speed = value);
    _onSliderChanged(_kParamSpeed, value);
  }

  void _onIntensityChanged(double value) {
    setState(() => _intensity = value);
    _onSliderChanged(_kParamIntensity, value);
  }

  void _resetAll() {
    setState(() {
      _rotationX = 0;
      _rotationY = 0;
      _positionX = 0;
      _positionY = 0;
      _scale = 1.0;
      _baseScale = 1.0;
      _speed = 1.0;
      _intensity = 0.5;
    });

    _send(UnityMessage.command('ResetParameters', {}));
  }

  // ---------------------------------------------------------------------------
  // Build
  // ---------------------------------------------------------------------------

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        title: const Text('Unity Kit'),
        backgroundColor: Colors.transparent,
        actions: [
          _StatusChip(isReady: _unityReady),
          const SizedBox(width: 4),
          IconButton(
            onPressed: () => setState(() => _useBinary = !_useBinary),
            icon: Icon(_useBinary ? Icons.memory : Icons.data_object),
            color: _useBinary ? Colors.tealAccent : null,
            tooltip: _useBinary ? 'Binary protocol (on)' : 'JSON protocol',
          ),
          PopupMenuButton<UnityArMode>(
            icon: Icon(
              _arMode == UnityArMode.none
                  ? Icons.view_in_ar_outlined
                  : Icons.view_in_ar,
              color: _arMode == UnityArMode.none ? null : Colors.tealAccent,
            ),
            tooltip: 'AR mode',
            onSelected: _onArModeSelected,
            itemBuilder: (context) => [
              for (final mode in UnityArMode.values)
                CheckedPopupMenuItem(
                  value: mode,
                  checked: mode == _arMode,
                  child: Text('AR: ${mode.name}'),
                ),
            ],
          ),
          IconButton(
            onPressed: _resetAll,
            icon: const Icon(Icons.refresh),
            tooltip: 'Reset all',
          ),
          const SizedBox(width: 4),
        ],
      ),
      body: Column(
        children: [
          // Unity view with gesture detection
          Expanded(child: _buildUnityArea()),

          // Gesture info + remaining sliders
          _ControlsPanel(
            rotationX: _rotationX,
            rotationY: _rotationY,
            positionX: _positionX,
            positionY: _positionY,
            scale: _scale,
            speed: _speed,
            intensity: _intensity,
            onSpeedChanged: _onSpeedChanged,
            onIntensityChanged: _onIntensityChanged,
            enabled: _unityReady,
          ),
        ],
      ),
    );
  }

  Widget _buildUnityArea() {
    if (kIsWeb ||
        !(defaultTargetPlatform == TargetPlatform.android ||
            defaultTargetPlatform == TargetPlatform.iOS)) {
      return const UnityPlaceholder(
        message: 'Unity view is not available on this platform.',
        backgroundColor: Colors.black,
        indicatorColor: Colors.deepPurple,
      );
    }

    final config = _arMode == UnityArMode.none
        ? const UnityConfig(
            sceneName: _kDefaultScene,
            fullscreen: false,
            platformViewMode: PlatformViewMode.hybridComposition,
            targetFrameRate: 60,
            unloadOnDispose: true,
          )
        : UnityConfig.ar(sceneName: _kDefaultScene, mode: _arMode);

    return Stack(
      children: [
        GestureDetector(
          onScaleStart: _onScaleStart,
          onScaleUpdate: _onScaleUpdate,
          child: UnityView(
            // Recreate the platform view when the AR mode changes.
            key: ValueKey(_arMode),
            config: config,
            gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
              Factory<ScaleGestureRecognizer>(ScaleGestureRecognizer.new),
            },
            placeholder: const UnityPlaceholder(
              message: 'Loading 3D view...',
              backgroundColor: Colors.black,
              indicatorColor: Colors.deepPurple,
            ),
            onReady: (bridge) {
              developer.log('UnityView ready', name: 'example');
              _perfSub?.cancel();
              _perfSub = bridge.performanceStream.listen((stats) {
                if (mounted) setState(() => _perf = stats);
              });
              setState(() {
                _unityReady = true;
                _bridge = bridge;
              });
            },
            onMessage: (message) {
              developer.log(
                'Unity message: ${message.type} ${message.data ?? ''}',
                name: 'example',
              );
            },
          ),
        ),
        if (_perf != null)
          Positioned(
            top: 8,
            left: 8,
            child: _PerformanceOverlay(stats: _perf!),
          ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Performance overlay -- live UnityPerformanceStats
// ---------------------------------------------------------------------------

class _PerformanceOverlay extends StatelessWidget {
  const _PerformanceOverlay({required this.stats});

  final UnityPerformanceStats stats;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
      decoration: BoxDecoration(
        color: Colors.black54,
        borderRadius: BorderRadius.circular(8),
      ),
      child: Text(
        '${stats.fps.toStringAsFixed(0)} fps  •  '
        '${stats.frameTimeMs.toStringAsFixed(1)} ms  •  '
        '${stats.usedMemoryMb.toStringAsFixed(0)} MB',
        style: const TextStyle(
          fontSize: 11,
          fontFamily: 'monospace',
          color: Colors.tealAccent,
        ),
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Status chip
// ---------------------------------------------------------------------------

class _StatusChip extends StatelessWidget {
  const _StatusChip({required this.isReady});

  final bool isReady;

  @override
  Widget build(BuildContext context) {
    final color = isReady ? Colors.green : Colors.orange;
    final label = isReady ? 'ready' : 'loading';
    return Chip(
      avatar: CircleAvatar(backgroundColor: color, radius: 6),
      label: Text(label, style: const TextStyle(fontSize: 12)),
      visualDensity: VisualDensity.compact,
    );
  }
}

// ---------------------------------------------------------------------------
// Controls panel -- gesture readouts + remaining sliders
// ---------------------------------------------------------------------------

class _ControlsPanel extends StatelessWidget {
  const _ControlsPanel({
    required this.rotationX,
    required this.rotationY,
    required this.positionX,
    required this.positionY,
    required this.scale,
    required this.speed,
    required this.intensity,
    required this.onSpeedChanged,
    required this.onIntensityChanged,
    required this.enabled,
  });

  final double rotationX;
  final double rotationY;
  final double positionX;
  final double positionY;
  final double scale;
  final double speed;
  final double intensity;
  final ValueChanged<double> onSpeedChanged;
  final ValueChanged<double> onIntensityChanged;
  final bool enabled;

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.grey[900],
        borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
      ),
      child: SafeArea(
        top: false,
        child: Padding(
          padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              // Drag handle
              Container(
                width: 40,
                height: 4,
                margin: const EdgeInsets.only(bottom: 12),
                decoration: BoxDecoration(
                  color: Colors.grey[700],
                  borderRadius: BorderRadius.circular(2),
                ),
              ),

              // Gesture readouts
              _GestureInfo(
                rotationX: rotationX,
                rotationY: rotationY,
                positionX: positionX,
                positionY: positionY,
                scale: scale,
              ),

              const SizedBox(height: 8),

              // Remaining sliders
              _SliderRow(
                label: 'Speed',
                value: speed,
                min: 0,
                max: 5.0,
                onChanged: enabled ? onSpeedChanged : null,
                valueLabel: '${speed.toStringAsFixed(1)}x',
              ),
              _SliderRow(
                label: 'Intensity',
                value: intensity,
                min: 0,
                max: 1.0,
                onChanged: enabled ? onIntensityChanged : null,
                valueLabel: '${(intensity * 100).toStringAsFixed(0)}%',
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Gesture info readout
// ---------------------------------------------------------------------------

class _GestureInfo extends StatelessWidget {
  const _GestureInfo({
    required this.rotationX,
    required this.rotationY,
    required this.positionX,
    required this.positionY,
    required this.scale,
  });

  final double rotationX;
  final double rotationY;
  final double positionX;
  final double positionY;
  final double scale;

  @override
  Widget build(BuildContext context) {
    const labelStyle = TextStyle(
      fontSize: 11,
      color: Colors.white38,
    );
    const valueStyle = TextStyle(
      fontSize: 11,
      fontFamily: 'monospace',
      color: Colors.white60,
    );

    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        _InfoItem(
          label: 'Rot',
          value:
              '${rotationX.toStringAsFixed(0)}, ${rotationY.toStringAsFixed(0)}',
          labelStyle: labelStyle,
          valueStyle: valueStyle,
        ),
        _InfoItem(
          label: 'Pos',
          value:
              '${positionX.toStringAsFixed(2)}, ${positionY.toStringAsFixed(2)}',
          labelStyle: labelStyle,
          valueStyle: valueStyle,
        ),
        _InfoItem(
          label: 'Scale',
          value: '${scale.toStringAsFixed(2)}x',
          labelStyle: labelStyle,
          valueStyle: valueStyle,
        ),
      ],
    );
  }
}

class _InfoItem extends StatelessWidget {
  const _InfoItem({
    required this.label,
    required this.value,
    required this.labelStyle,
    required this.valueStyle,
  });

  final String label;
  final String value;
  final TextStyle labelStyle;
  final TextStyle valueStyle;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Text(label, style: labelStyle),
        const SizedBox(height: 2),
        Text(value, style: valueStyle),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Single slider row
// ---------------------------------------------------------------------------

class _SliderRow extends StatelessWidget {
  const _SliderRow({
    required this.label,
    required this.value,
    required this.min,
    required this.max,
    required this.onChanged,
    required this.valueLabel,
  });

  final String label;
  final double value;
  final double min;
  final double max;
  final ValueChanged<double>? onChanged;
  final String valueLabel;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Row(
        children: [
          SizedBox(
            width: 80,
            child: Text(
              label,
              style: TextStyle(
                fontSize: 12,
                color: onChanged != null ? Colors.white70 : Colors.white30,
              ),
            ),
          ),
          Expanded(
            child: SliderTheme(
              data: SliderThemeData(
                trackHeight: 3,
                thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 7),
                overlayShape: const RoundSliderOverlayShape(overlayRadius: 14),
                activeTrackColor: Colors.deepPurple[300],
                inactiveTrackColor: Colors.grey[800],
                thumbColor: Colors.deepPurple[200],
                disabledActiveTrackColor: Colors.grey[700],
                disabledInactiveTrackColor: Colors.grey[850],
                disabledThumbColor: Colors.grey[600],
              ),
              child: Slider(
                value: value,
                min: min,
                max: max,
                onChanged: onChanged,
              ),
            ),
          ),
          SizedBox(
            width: 48,
            child: Text(
              valueLabel,
              textAlign: TextAlign.right,
              style: TextStyle(
                fontSize: 12,
                fontFamily: 'monospace',
                color: onChanged != null ? Colors.white70 : Colors.white30,
              ),
            ),
          ),
        ],
      ),
    );
  }
}
20
likes
150
points
66.6k
downloads

Documentation

API reference

Publisher

verified publisherravenlab.tech

Weekly Downloads

Flutter plugin for Unity 3D integration — typed bridge, binary protocol, performance + AR, on Android, iOS, web & desktop.

Repository (GitHub)
View/report issues

Topics

#unity #flutter-unity #game #assets #bridge

License

MIT (license)

Dependencies

crypto, flutter, flutter_plugin_android_lifecycle, flutter_web_plugins, http, path_provider, web

More

Packages that depend on unity_kit

Packages that implement unity_kit