sense_the_rythm

rythm game for ESense Earable
git clone git://source.orangerot.dev:/university/sense_the_rythm.git
Log | Files | Refs | README | LICENSE

level.dart (8494B)


      1 import 'dart:async';
      2 
      3 import 'package:flutter/material.dart';
      4 import 'package:audioplayers/audioplayers.dart';
      5 import 'package:flutter/services.dart';
      6 import 'package:sense_the_rhythm/models/arrow_direction.dart';
      7 import 'package:sense_the_rhythm/models/input_direction.dart';
      8 import 'package:sense_the_rhythm/models/note.dart';
      9 import 'package:sense_the_rhythm/utils/esense_input.dart';
     10 import 'package:sense_the_rhythm/utils/simfile.dart';
     11 import 'package:sense_the_rhythm/widgets/arrows.dart';
     12 import 'package:sense_the_rhythm/screens/game_over.dart';
     13 
     14 class Level extends StatefulWidget {
     15   const Level(this.simfile, {super.key});
     16   final Simfile simfile;
     17 
     18   @override
     19   State<Level> createState() => _LevelState();
     20 }
     21 
     22 class _LevelState extends State<Level> with SingleTickerProviderStateMixin {
     23   final _player = AudioPlayer();
     24   bool _isPlaying = true;
     25   Duration? _duration;
     26   Duration? _position;
     27 
     28   StreamSubscription? _durationSubscription;
     29   StreamSubscription? _positionSubscription;
     30   StreamSubscription? _buttonSubscription;
     31 
     32   final FocusNode _focusNode = FocusNode();
     33   final InputDirection _inputDirection = InputDirection();
     34 
     35   String _hitOrMissMessage = 'Play!';
     36 
     37   final List<Note> _notes = [];
     38 
     39   late AnimationController _animationController;
     40   late Animation<double> _animation;
     41 
     42   @override
     43   void setState(VoidCallback fn) {
     44     // Subscriptions only can be closed asynchronously,
     45     // therefore events can occur after widget has been disposed.
     46     if (mounted) {
     47       super.setState(fn);
     48     }
     49   }
     50 
     51   @override
     52   void initState() {
     53     super.initState();
     54     ESenseInput.instance.resetAngles();
     55 
     56     _animationController = AnimationController(
     57       vsync: this,
     58       duration: Duration(seconds: 2),
     59     );
     60     _animation =
     61         Tween<double>(begin: 1.0, end: 0.0).animate(_animationController);
     62     _animationController.forward();
     63 
     64     // Use initial values from player
     65     _player.getDuration().then(
     66           (value) => setState(() {
     67             _duration = value;
     68           }),
     69         );
     70     _player.getCurrentPosition().then(
     71           (value) => setState(() {
     72             _position = value;
     73           }),
     74         );
     75 
     76     // listen for new values from player
     77     _durationSubscription =
     78         _player.onDurationChanged.listen((Duration duration) {
     79       setState(() => _duration = duration);
     80     });
     81 
     82     _positionSubscription =
     83         _player.onPositionChanged.listen((Duration position) {
     84       setState(() => _position = position);
     85       for (final note in _notes) {
     86         _noteHitCheck(note, position);
     87       }
     88     });
     89 
     90     // go to GameOverStats when level finishes
     91     _player.onPlayerComplete.listen((void _) {
     92       Route route = MaterialPageRoute(
     93           builder: (context) => GameOverStats(
     94                 simfile: widget.simfile,
     95                 notes: _notes,
     96               ));
     97       Navigator.pushReplacement(context, route);
     98     });
     99 
    100     // listen for esense button and pause/resume
    101     if (ESenseInput.instance.connected) {
    102       _buttonSubscription = ESenseInput.instance.buttonEvents().listen((event) {
    103         if (!event.pressed) {
    104           _pauseResume();
    105         }
    106       });
    107     }
    108 
    109     // convert beats to notes
    110     widget.simfile.chartSimplest?.beats.forEach((time, noteData) {
    111       int arrowIndex = noteData.indexOf('1');
    112       if (arrowIndex < 0 || arrowIndex > 3) {
    113         return;
    114       }
    115       _notes.add(Note(time: time, direction: ArrowDirection.values[arrowIndex]));
    116     });
    117 
    118     _player.play(DeviceFileSource(widget.simfile.audioPath!));
    119   }
    120 
    121   @override
    122   void dispose() {
    123     _animationController.dispose();
    124     _durationSubscription?.cancel();
    125     _positionSubscription?.cancel();
    126     _buttonSubscription?.cancel();
    127     _player.dispose();
    128     super.dispose();
    129   }
    130 
    131   /// toggle between pause and resume
    132   void _pauseResume() {
    133     if (_isPlaying) {
    134       _player.pause();
    135       setState(() {
    136         _isPlaying = false;
    137       });
    138     } else {
    139       _player.resume();
    140       setState(() {
    141         _isPlaying = true;
    142       });
    143     }
    144   }
    145 
    146   /// checks if the [note] is hit on [time] with the correct InputDirection
    147   void _noteHitCheck(Note note, Duration time) {
    148     note.position = note.time - time.inMilliseconds / 60000.0;
    149     if (note.wasHit != null) {
    150       return;
    151     }
    152 
    153     // you have +- half a second to hit
    154     if (note.position.abs() < 0.5 * 1.0 / 60.0) {
    155       // combine keyboard and esense input
    156       InputDirection esenseDirection =
    157           ESenseInput.instance.getInputDirection(note.direction);
    158       _inputDirection.up |= esenseDirection.up;
    159       _inputDirection.down |= esenseDirection.down;
    160       _inputDirection.left |= esenseDirection.left;
    161       _inputDirection.right |= esenseDirection.right;
    162 
    163       // check if input matches arrow direction
    164       bool keypressCorrect = false;
    165       switch (note.direction) {
    166         case ArrowDirection.up:
    167           keypressCorrect = _inputDirection.up;
    168           break;
    169         case ArrowDirection.down:
    170           keypressCorrect = _inputDirection.down;
    171           break;
    172         case ArrowDirection.right:
    173           keypressCorrect = _inputDirection.right;
    174           break;
    175         case ArrowDirection.left:
    176           keypressCorrect = _inputDirection.left;
    177           break;
    178       }
    179       if (keypressCorrect) {
    180         print("you hit!");
    181         note.wasHit = true;
    182         _animationController.reset();
    183         _animationController.forward();
    184         _inputDirection.reset();
    185         setState(() {
    186           _hitOrMissMessage = 'Great!';
    187         });
    188       }
    189     } else if (note.position < -0.5 * 1.0 / 60.0) {
    190       print("Missed");
    191       note.wasHit = false;
    192       _animationController.reset();
    193       _animationController.forward();
    194       _inputDirection.reset();
    195       setState(() {
    196         _hitOrMissMessage = 'Missed';
    197       });
    198     }
    199   }
    200 
    201   /// sets the InputDirection based on the arrow keys
    202   void _keyboardHandler(event) {
    203     bool isDown = false;
    204     if (event is KeyDownEvent) {
    205       isDown = true;
    206     } else if (event is KeyUpEvent) {
    207       isDown = false;
    208     } else {
    209       return;
    210     }
    211     switch (event.logicalKey) {
    212       case LogicalKeyboardKey.arrowUp:
    213         _inputDirection.up = isDown;
    214         break;
    215       case LogicalKeyboardKey.arrowDown:
    216         _inputDirection.down = isDown;
    217         break;
    218       case LogicalKeyboardKey.arrowLeft:
    219         _inputDirection.left = isDown;
    220         break;
    221       case LogicalKeyboardKey.arrowRight:
    222         _inputDirection.right = isDown;
    223         break;
    224     }
    225   }
    226 
    227   @override
    228   Widget build(BuildContext context) {
    229     return KeyboardListener(
    230       focusNode: _focusNode,
    231       autofocus: true,
    232       onKeyEvent: _keyboardHandler,
    233       child: Scaffold(
    234           appBar: AppBar(
    235             leading: IconButton(
    236               icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow),
    237               onPressed: _pauseResume,
    238             ),
    239             title: Text(widget.simfile.tags['TITLE']!),
    240             actions: [
    241               IconButton(
    242                   icon: Icon(Icons.close),
    243                   onPressed: () => Navigator.pop(context))
    244             ],
    245             bottom: PreferredSize(
    246                 preferredSize: Size(double.infinity, 1.0),
    247                 child: LinearProgressIndicator(
    248                   value: (_duration != null &&
    249                           _position != null &&
    250                           _position!.inMilliseconds > 0 &&
    251                           _position!.inMilliseconds < _duration!.inMilliseconds)
    252                       ? _position!.inMilliseconds / _duration!.inMilliseconds
    253                       : 0.0,
    254                 )),
    255           ),
    256           body: Stack(children: [
    257             Arrows(notes: _notes),
    258             Positioned(
    259               top: 50,
    260               width: MediaQuery.of(context).size.width,
    261               left: 0,
    262               child: FadeTransition(
    263                 opacity: _animation,
    264                 child: Text(
    265                   _hitOrMissMessage,
    266                   textScaler: TextScaler.linear(4),
    267                   textAlign: TextAlign.center,
    268                 ),
    269               ),
    270             ),
    271             Positioned(
    272               left: MediaQuery.of(context).size.width / 2 - 50,
    273               bottom: 50,
    274               child: Container(
    275                 width: 100,
    276                 height: 100,
    277                 decoration: BoxDecoration(
    278                     shape: BoxShape.circle,
    279                     border: Border.all(color: Colors.black, width: 10)),
    280               ),
    281             ),
    282           ])),
    283     );
    284   }
    285 }