bastd.actor.controlsguide

Defines Actors related to controls guides.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Defines Actors related to controls guides."""
  4
  5from __future__ import annotations
  6
  7from typing import TYPE_CHECKING
  8
  9import ba
 10import ba.internal
 11
 12if TYPE_CHECKING:
 13    from typing import Any, Sequence
 14
 15
 16class ControlsGuide(ba.Actor):
 17    """A screen overlay of game controls.
 18
 19    category: Gameplay Classes
 20
 21    Shows button mappings based on what controllers are connected.
 22    Handy to show at the start of a series or whenever there might
 23    be newbies watching.
 24    """
 25
 26    def __init__(
 27        self,
 28        position: tuple[float, float] = (390.0, 120.0),
 29        scale: float = 1.0,
 30        delay: float = 0.0,
 31        lifespan: float | None = None,
 32        bright: bool = False,
 33    ):
 34        """Instantiate an overlay.
 35
 36        delay: is the time in seconds before the overlay fades in.
 37
 38        lifespan: if not None, the overlay will fade back out and die after
 39                  that long (in milliseconds).
 40
 41        bright: if True, brighter colors will be used; handy when showing
 42                over gameplay but may be too bright for join-screens, etc.
 43        """
 44        # pylint: disable=too-many-statements
 45        # pylint: disable=too-many-locals
 46        super().__init__()
 47        show_title = True
 48        scale *= 0.75
 49        image_size = 90.0 * scale
 50        offs = 74.0 * scale
 51        offs5 = 43.0 * scale
 52        ouya = False
 53        maxw = 50
 54        self._lifespan = lifespan
 55        self._dead = False
 56        self._bright = bright
 57        self._cancel_timer: ba.Timer | None = None
 58        self._fade_in_timer: ba.Timer | None = None
 59        self._update_timer: ba.Timer | None = None
 60        self._title_text: ba.Node | None
 61        clr: Sequence[float]
 62        extra_pos_1: tuple[float, float] | None
 63        extra_pos_2: tuple[float, float] | None
 64        if ba.app.iircade_mode:
 65            xtweak = 0.2
 66            ytweak = 0.2
 67            jump_pos = (
 68                position[0] + offs * (-1.2 + xtweak),
 69                position[1] + offs * (0.1 + ytweak),
 70            )
 71            bomb_pos = (
 72                position[0] + offs * (0.0 + xtweak),
 73                position[1] + offs * (0.5 + ytweak),
 74            )
 75            punch_pos = (
 76                position[0] + offs * (1.2 + xtweak),
 77                position[1] + offs * (0.5 + ytweak),
 78            )
 79
 80            pickup_pos = (
 81                position[0] + offs * (-1.4 + xtweak),
 82                position[1] + offs * (-1.2 + ytweak),
 83            )
 84            extra_pos_1 = (
 85                position[0] + offs * (-0.2 + xtweak),
 86                position[1] + offs * (-0.8 + ytweak),
 87            )
 88            extra_pos_2 = (
 89                position[0] + offs * (1.0 + xtweak),
 90                position[1] + offs * (-0.8 + ytweak),
 91            )
 92            self._force_hide_button_names = True
 93        else:
 94            punch_pos = (position[0] - offs * 1.1, position[1])
 95            jump_pos = (position[0], position[1] - offs)
 96            bomb_pos = (position[0] + offs * 1.1, position[1])
 97            pickup_pos = (position[0], position[1] + offs)
 98            extra_pos_1 = None
 99            extra_pos_2 = None
100            self._force_hide_button_names = False
101
102        if show_title:
103            self._title_text_pos_top = (
104                position[0],
105                position[1] + 139.0 * scale,
106            )
107            self._title_text_pos_bottom = (
108                position[0],
109                position[1] + 139.0 * scale,
110            )
111            clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7)
112            tval = ba.Lstr(
113                value='${A}:', subs=[('${A}', ba.Lstr(resource='controlsText'))]
114            )
115            self._title_text = ba.newnode(
116                'text',
117                attrs={
118                    'text': tval,
119                    'host_only': True,
120                    'scale': 1.1 * scale,
121                    'shadow': 0.5,
122                    'flatness': 1.0,
123                    'maxwidth': 480,
124                    'v_align': 'center',
125                    'h_align': 'center',
126                    'color': clr,
127                },
128            )
129        else:
130            self._title_text = None
131        pos = jump_pos
132        clr = (0.4, 1, 0.4)
133        self._jump_image = ba.newnode(
134            'image',
135            attrs={
136                'texture': ba.gettexture('buttonJump'),
137                'absolute_scale': True,
138                'host_only': True,
139                'vr_depth': 10,
140                'position': pos,
141                'scale': (image_size, image_size),
142                'color': clr,
143            },
144        )
145        self._jump_text = ba.newnode(
146            'text',
147            attrs={
148                'v_align': 'top',
149                'h_align': 'center',
150                'scale': 1.5 * scale,
151                'flatness': 1.0,
152                'host_only': True,
153                'shadow': 1.0,
154                'maxwidth': maxw,
155                'position': (pos[0], pos[1] - offs5),
156                'color': clr,
157            },
158        )
159        clr = (0.2, 0.6, 1) if ouya else (1, 0.7, 0.3)
160        pos = punch_pos
161        self._punch_image = ba.newnode(
162            'image',
163            attrs={
164                'texture': ba.gettexture('buttonPunch'),
165                'absolute_scale': True,
166                'host_only': True,
167                'vr_depth': 10,
168                'position': pos,
169                'scale': (image_size, image_size),
170                'color': clr,
171            },
172        )
173        self._punch_text = ba.newnode(
174            'text',
175            attrs={
176                'v_align': 'top',
177                'h_align': 'center',
178                'scale': 1.5 * scale,
179                'flatness': 1.0,
180                'host_only': True,
181                'shadow': 1.0,
182                'maxwidth': maxw,
183                'position': (pos[0], pos[1] - offs5),
184                'color': clr,
185            },
186        )
187        pos = bomb_pos
188        clr = (1, 0.3, 0.3)
189        self._bomb_image = ba.newnode(
190            'image',
191            attrs={
192                'texture': ba.gettexture('buttonBomb'),
193                'absolute_scale': True,
194                'host_only': True,
195                'vr_depth': 10,
196                'position': pos,
197                'scale': (image_size, image_size),
198                'color': clr,
199            },
200        )
201        self._bomb_text = ba.newnode(
202            'text',
203            attrs={
204                'h_align': 'center',
205                'v_align': 'top',
206                'scale': 1.5 * scale,
207                'flatness': 1.0,
208                'host_only': True,
209                'shadow': 1.0,
210                'maxwidth': maxw,
211                'position': (pos[0], pos[1] - offs5),
212                'color': clr,
213            },
214        )
215        pos = pickup_pos
216        clr = (1, 0.8, 0.3) if ouya else (0.8, 0.5, 1)
217        self._pickup_image = ba.newnode(
218            'image',
219            attrs={
220                'texture': ba.gettexture('buttonPickUp'),
221                'absolute_scale': True,
222                'host_only': True,
223                'vr_depth': 10,
224                'position': pos,
225                'scale': (image_size, image_size),
226                'color': clr,
227            },
228        )
229        self._pick_up_text = ba.newnode(
230            'text',
231            attrs={
232                'v_align': 'top',
233                'h_align': 'center',
234                'scale': 1.5 * scale,
235                'flatness': 1.0,
236                'host_only': True,
237                'shadow': 1.0,
238                'maxwidth': maxw,
239                'position': (pos[0], pos[1] - offs5),
240                'color': clr,
241            },
242        )
243        clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0)
244        self._run_text_pos_top = (position[0], position[1] - 135.0 * scale)
245        self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale)
246        sval = 1.0 * scale if ba.app.vr_mode else 0.8 * scale
247        self._run_text = ba.newnode(
248            'text',
249            attrs={
250                'scale': sval,
251                'host_only': True,
252                'shadow': 1.0 if ba.app.vr_mode else 0.5,
253                'flatness': 1.0,
254                'maxwidth': 380,
255                'v_align': 'top',
256                'h_align': 'center',
257                'color': clr,
258            },
259        )
260        clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7)
261        self._extra_text = ba.newnode(
262            'text',
263            attrs={
264                'scale': 0.8 * scale,
265                'host_only': True,
266                'shadow': 0.5,
267                'flatness': 1.0,
268                'maxwidth': 380,
269                'v_align': 'top',
270                'h_align': 'center',
271                'color': clr,
272            },
273        )
274
275        if extra_pos_1 is not None:
276            self._extra_image_1: ba.Node | None = ba.newnode(
277                'image',
278                attrs={
279                    'texture': ba.gettexture('nub'),
280                    'absolute_scale': True,
281                    'host_only': True,
282                    'vr_depth': 10,
283                    'position': extra_pos_1,
284                    'scale': (image_size, image_size),
285                    'color': (0.5, 0.5, 0.5),
286                },
287            )
288        else:
289            self._extra_image_1 = None
290        if extra_pos_2 is not None:
291            self._extra_image_2: ba.Node | None = ba.newnode(
292                'image',
293                attrs={
294                    'texture': ba.gettexture('nub'),
295                    'absolute_scale': True,
296                    'host_only': True,
297                    'vr_depth': 10,
298                    'position': extra_pos_2,
299                    'scale': (image_size, image_size),
300                    'color': (0.5, 0.5, 0.5),
301                },
302            )
303        else:
304            self._extra_image_2 = None
305
306        self._nodes = [
307            self._bomb_image,
308            self._bomb_text,
309            self._punch_image,
310            self._punch_text,
311            self._jump_image,
312            self._jump_text,
313            self._pickup_image,
314            self._pick_up_text,
315            self._run_text,
316            self._extra_text,
317        ]
318        if show_title:
319            assert self._title_text
320            self._nodes.append(self._title_text)
321        if self._extra_image_1 is not None:
322            self._nodes.append(self._extra_image_1)
323        if self._extra_image_2 is not None:
324            self._nodes.append(self._extra_image_2)
325
326        # Start everything invisible.
327        for node in self._nodes:
328            node.opacity = 0.0
329
330        # Don't do anything until our delay has passed.
331        ba.timer(delay, ba.WeakCall(self._start_updating))
332
333    @staticmethod
334    def _meaningful_button_name(device: ba.InputDevice, button: int) -> str:
335        """Return a flattened string button name; empty for non-meaningful."""
336        if not device.has_meaningful_button_names:
337            return ''
338        return device.get_button_name(button).evaluate()
339
340    def _start_updating(self) -> None:
341
342        # Ok, our delay has passed. Now lets periodically see if we can fade
343        # in (if a touch-screen is present we only want to show up if gamepads
344        # are connected, etc).
345        # Also set up a timer so if we haven't faded in by the end of our
346        # duration, abort.
347        if self._lifespan is not None:
348            self._cancel_timer = ba.Timer(
349                self._lifespan,
350                ba.WeakCall(self.handlemessage, ba.DieMessage(immediate=True)),
351            )
352        self._fade_in_timer = ba.Timer(
353            1.0, ba.WeakCall(self._check_fade_in), repeat=True
354        )
355        self._check_fade_in()  # Do one check immediately.
356
357    def _check_fade_in(self) -> None:
358        from ba.internal import get_device_value
359
360        # If we have a touchscreen, we only fade in if we have a player with
361        # an input device that is *not* the touchscreen.
362        # (otherwise it is confusing to see the touchscreen buttons right
363        # next to our display buttons)
364        touchscreen: ba.InputDevice | None = ba.internal.getinputdevice(
365            'TouchScreen', '#1', doraise=False
366        )
367
368        if touchscreen is not None:
369            # We look at the session's players; not the activity's.
370            # We want to get ones who are still in the process of
371            # selecting a character, etc.
372            input_devices = [
373                p.inputdevice for p in ba.getsession().sessionplayers
374            ]
375            input_devices = [
376                i for i in input_devices if i and i is not touchscreen
377            ]
378            fade_in = False
379            if input_devices:
380                # Only count this one if it has non-empty button names
381                # (filters out wiimotes, the remote-app, etc).
382                for device in input_devices:
383                    for name in (
384                        'buttonPunch',
385                        'buttonJump',
386                        'buttonBomb',
387                        'buttonPickUp',
388                    ):
389                        if (
390                            self._meaningful_button_name(
391                                device, get_device_value(device, name)
392                            )
393                            != ''
394                        ):
395                            fade_in = True
396                            break
397                    if fade_in:
398                        break  # No need to keep looking.
399        else:
400            # No touch-screen; fade in immediately.
401            fade_in = True
402        if fade_in:
403            self._cancel_timer = None  # Didn't need this.
404            self._fade_in_timer = None  # Done with this.
405            self._fade_in()
406
407    def _fade_in(self) -> None:
408        for node in self._nodes:
409            ba.animate(node, 'opacity', {0: 0.0, 2.0: 1.0})
410
411        # If we were given a lifespan, transition out after it.
412        if self._lifespan is not None:
413            ba.timer(
414                self._lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage())
415            )
416        self._update()
417        self._update_timer = ba.Timer(
418            1.0, ba.WeakCall(self._update), repeat=True
419        )
420
421    def _update(self) -> None:
422        # pylint: disable=too-many-statements
423        # pylint: disable=too-many-branches
424        # pylint: disable=too-many-locals
425        from ba.internal import get_device_value, get_remote_app_name
426
427        if self._dead:
428            return
429        punch_button_names = set()
430        jump_button_names = set()
431        pickup_button_names = set()
432        bomb_button_names = set()
433
434        # We look at the session's players; not the activity's - we want to
435        # get ones who are still in the process of selecting a character, etc.
436        input_devices = [p.inputdevice for p in ba.getsession().sessionplayers]
437        input_devices = [i for i in input_devices if i]
438
439        # If there's no players with input devices yet, try to default to
440        # showing keyboard controls.
441        if not input_devices:
442            kbd = ba.internal.getinputdevice('Keyboard', '#1', doraise=False)
443            if kbd is not None:
444                input_devices.append(kbd)
445
446        # We word things specially if we have nothing but keyboards.
447        all_keyboards = input_devices and all(
448            i.name == 'Keyboard' for i in input_devices
449        )
450        only_remote = len(input_devices) == 1 and all(
451            i.name == 'Amazon Fire TV Remote' for i in input_devices
452        )
453
454        right_button_names = set()
455        left_button_names = set()
456        up_button_names = set()
457        down_button_names = set()
458
459        # For each player in the game with an input device,
460        # get the name of the button for each of these 4 actions.
461        # If any of them are uniform across all devices, display the name.
462        for device in input_devices:
463            # We only care about movement buttons in the case of keyboards.
464            if all_keyboards:
465                right_button_names.add(
466                    device.get_button_name(
467                        get_device_value(device, 'buttonRight')
468                    )
469                )
470                left_button_names.add(
471                    device.get_button_name(
472                        get_device_value(device, 'buttonLeft')
473                    )
474                )
475                down_button_names.add(
476                    device.get_button_name(
477                        get_device_value(device, 'buttonDown')
478                    )
479                )
480                up_button_names.add(
481                    device.get_button_name(get_device_value(device, 'buttonUp'))
482                )
483
484            # Ignore empty values; things like the remote app or
485            # wiimotes can return these.
486            bname = self._meaningful_button_name(
487                device, get_device_value(device, 'buttonPunch')
488            )
489            if bname != '':
490                punch_button_names.add(bname)
491            bname = self._meaningful_button_name(
492                device, get_device_value(device, 'buttonJump')
493            )
494            if bname != '':
495                jump_button_names.add(bname)
496            bname = self._meaningful_button_name(
497                device, get_device_value(device, 'buttonBomb')
498            )
499            if bname != '':
500                bomb_button_names.add(bname)
501            bname = self._meaningful_button_name(
502                device, get_device_value(device, 'buttonPickUp')
503            )
504            if bname != '':
505                pickup_button_names.add(bname)
506
507        # If we have no values yet, we may want to throw out some sane
508        # defaults.
509        if all(
510            not lst
511            for lst in (
512                punch_button_names,
513                jump_button_names,
514                bomb_button_names,
515                pickup_button_names,
516            )
517        ):
518            # Otherwise on android show standard buttons.
519            if ba.app.platform == 'android':
520                punch_button_names.add('X')
521                jump_button_names.add('A')
522                bomb_button_names.add('B')
523                pickup_button_names.add('Y')
524
525        run_text = ba.Lstr(
526            value='${R}: ${B}',
527            subs=[
528                ('${R}', ba.Lstr(resource='runText')),
529                (
530                    '${B}',
531                    ba.Lstr(
532                        resource='holdAnyKeyText'
533                        if all_keyboards
534                        else 'holdAnyButtonText'
535                    ),
536                ),
537            ],
538        )
539
540        # If we're all keyboards, lets show move keys too.
541        if (
542            all_keyboards
543            and len(up_button_names) == 1
544            and len(down_button_names) == 1
545            and len(left_button_names) == 1
546            and len(right_button_names) == 1
547        ):
548            up_text = list(up_button_names)[0]
549            down_text = list(down_button_names)[0]
550            left_text = list(left_button_names)[0]
551            right_text = list(right_button_names)[0]
552            run_text = ba.Lstr(
553                value='${M}: ${U}, ${L}, ${D}, ${R}\n${RUN}',
554                subs=[
555                    ('${M}', ba.Lstr(resource='moveText')),
556                    ('${U}', up_text),
557                    ('${L}', left_text),
558                    ('${D}', down_text),
559                    ('${R}', right_text),
560                    ('${RUN}', run_text),
561                ],
562            )
563
564        if self._force_hide_button_names:
565            jump_button_names.clear()
566            punch_button_names.clear()
567            bomb_button_names.clear()
568            pickup_button_names.clear()
569
570        self._run_text.text = run_text
571        w_text: ba.Lstr | str
572        if only_remote and self._lifespan is None:
573            w_text = ba.Lstr(
574                resource='fireTVRemoteWarningText',
575                subs=[('${REMOTE_APP_NAME}', get_remote_app_name())],
576            )
577        else:
578            w_text = ''
579        self._extra_text.text = w_text
580        if len(punch_button_names) == 1:
581            self._punch_text.text = list(punch_button_names)[0]
582        else:
583            self._punch_text.text = ''
584
585        if len(jump_button_names) == 1:
586            tval = list(jump_button_names)[0]
587        else:
588            tval = ''
589        self._jump_text.text = tval
590        if tval == '':
591            self._run_text.position = self._run_text_pos_top
592            self._extra_text.position = (
593                self._run_text_pos_top[0],
594                self._run_text_pos_top[1] - 50,
595            )
596        else:
597            self._run_text.position = self._run_text_pos_bottom
598            self._extra_text.position = (
599                self._run_text_pos_bottom[0],
600                self._run_text_pos_bottom[1] - 50,
601            )
602        if len(bomb_button_names) == 1:
603            self._bomb_text.text = list(bomb_button_names)[0]
604        else:
605            self._bomb_text.text = ''
606
607        # Also move our title up/down depending on if this is shown.
608        if len(pickup_button_names) == 1:
609            self._pick_up_text.text = list(pickup_button_names)[0]
610            if self._title_text is not None:
611                self._title_text.position = self._title_text_pos_top
612        else:
613            self._pick_up_text.text = ''
614            if self._title_text is not None:
615                self._title_text.position = self._title_text_pos_bottom
616
617    def _die(self) -> None:
618        for node in self._nodes:
619            node.delete()
620        self._nodes = []
621        self._update_timer = None
622        self._dead = True
623
624    def exists(self) -> bool:
625        return not self._dead
626
627    def handlemessage(self, msg: Any) -> Any:
628        assert not self.expired
629        if isinstance(msg, ba.DieMessage):
630            if msg.immediate:
631                self._die()
632            else:
633                # If they don't need immediate,
634                # fade out our nodes and die later.
635                for node in self._nodes:
636                    ba.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0})
637                ba.timer(3.1, ba.WeakCall(self._die))
638            return None
639        return super().handlemessage(msg)
class ControlsGuide(ba._actor.Actor):
 17class ControlsGuide(ba.Actor):
 18    """A screen overlay of game controls.
 19
 20    category: Gameplay Classes
 21
 22    Shows button mappings based on what controllers are connected.
 23    Handy to show at the start of a series or whenever there might
 24    be newbies watching.
 25    """
 26
 27    def __init__(
 28        self,
 29        position: tuple[float, float] = (390.0, 120.0),
 30        scale: float = 1.0,
 31        delay: float = 0.0,
 32        lifespan: float | None = None,
 33        bright: bool = False,
 34    ):
 35        """Instantiate an overlay.
 36
 37        delay: is the time in seconds before the overlay fades in.
 38
 39        lifespan: if not None, the overlay will fade back out and die after
 40                  that long (in milliseconds).
 41
 42        bright: if True, brighter colors will be used; handy when showing
 43                over gameplay but may be too bright for join-screens, etc.
 44        """
 45        # pylint: disable=too-many-statements
 46        # pylint: disable=too-many-locals
 47        super().__init__()
 48        show_title = True
 49        scale *= 0.75
 50        image_size = 90.0 * scale
 51        offs = 74.0 * scale
 52        offs5 = 43.0 * scale
 53        ouya = False
 54        maxw = 50
 55        self._lifespan = lifespan
 56        self._dead = False
 57        self._bright = bright
 58        self._cancel_timer: ba.Timer | None = None
 59        self._fade_in_timer: ba.Timer | None = None
 60        self._update_timer: ba.Timer | None = None
 61        self._title_text: ba.Node | None
 62        clr: Sequence[float]
 63        extra_pos_1: tuple[float, float] | None
 64        extra_pos_2: tuple[float, float] | None
 65        if ba.app.iircade_mode:
 66            xtweak = 0.2
 67            ytweak = 0.2
 68            jump_pos = (
 69                position[0] + offs * (-1.2 + xtweak),
 70                position[1] + offs * (0.1 + ytweak),
 71            )
 72            bomb_pos = (
 73                position[0] + offs * (0.0 + xtweak),
 74                position[1] + offs * (0.5 + ytweak),
 75            )
 76            punch_pos = (
 77                position[0] + offs * (1.2 + xtweak),
 78                position[1] + offs * (0.5 + ytweak),
 79            )
 80
 81            pickup_pos = (
 82                position[0] + offs * (-1.4 + xtweak),
 83                position[1] + offs * (-1.2 + ytweak),
 84            )
 85            extra_pos_1 = (
 86                position[0] + offs * (-0.2 + xtweak),
 87                position[1] + offs * (-0.8 + ytweak),
 88            )
 89            extra_pos_2 = (
 90                position[0] + offs * (1.0 + xtweak),
 91                position[1] + offs * (-0.8 + ytweak),
 92            )
 93            self._force_hide_button_names = True
 94        else:
 95            punch_pos = (position[0] - offs * 1.1, position[1])
 96            jump_pos = (position[0], position[1] - offs)
 97            bomb_pos = (position[0] + offs * 1.1, position[1])
 98            pickup_pos = (position[0], position[1] + offs)
 99            extra_pos_1 = None
100            extra_pos_2 = None
101            self._force_hide_button_names = False
102
103        if show_title:
104            self._title_text_pos_top = (
105                position[0],
106                position[1] + 139.0 * scale,
107            )
108            self._title_text_pos_bottom = (
109                position[0],
110                position[1] + 139.0 * scale,
111            )
112            clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7)
113            tval = ba.Lstr(
114                value='${A}:', subs=[('${A}', ba.Lstr(resource='controlsText'))]
115            )
116            self._title_text = ba.newnode(
117                'text',
118                attrs={
119                    'text': tval,
120                    'host_only': True,
121                    'scale': 1.1 * scale,
122                    'shadow': 0.5,
123                    'flatness': 1.0,
124                    'maxwidth': 480,
125                    'v_align': 'center',
126                    'h_align': 'center',
127                    'color': clr,
128                },
129            )
130        else:
131            self._title_text = None
132        pos = jump_pos
133        clr = (0.4, 1, 0.4)
134        self._jump_image = ba.newnode(
135            'image',
136            attrs={
137                'texture': ba.gettexture('buttonJump'),
138                'absolute_scale': True,
139                'host_only': True,
140                'vr_depth': 10,
141                'position': pos,
142                'scale': (image_size, image_size),
143                'color': clr,
144            },
145        )
146        self._jump_text = ba.newnode(
147            'text',
148            attrs={
149                'v_align': 'top',
150                'h_align': 'center',
151                'scale': 1.5 * scale,
152                'flatness': 1.0,
153                'host_only': True,
154                'shadow': 1.0,
155                'maxwidth': maxw,
156                'position': (pos[0], pos[1] - offs5),
157                'color': clr,
158            },
159        )
160        clr = (0.2, 0.6, 1) if ouya else (1, 0.7, 0.3)
161        pos = punch_pos
162        self._punch_image = ba.newnode(
163            'image',
164            attrs={
165                'texture': ba.gettexture('buttonPunch'),
166                'absolute_scale': True,
167                'host_only': True,
168                'vr_depth': 10,
169                'position': pos,
170                'scale': (image_size, image_size),
171                'color': clr,
172            },
173        )
174        self._punch_text = ba.newnode(
175            'text',
176            attrs={
177                'v_align': 'top',
178                'h_align': 'center',
179                'scale': 1.5 * scale,
180                'flatness': 1.0,
181                'host_only': True,
182                'shadow': 1.0,
183                'maxwidth': maxw,
184                'position': (pos[0], pos[1] - offs5),
185                'color': clr,
186            },
187        )
188        pos = bomb_pos
189        clr = (1, 0.3, 0.3)
190        self._bomb_image = ba.newnode(
191            'image',
192            attrs={
193                'texture': ba.gettexture('buttonBomb'),
194                'absolute_scale': True,
195                'host_only': True,
196                'vr_depth': 10,
197                'position': pos,
198                'scale': (image_size, image_size),
199                'color': clr,
200            },
201        )
202        self._bomb_text = ba.newnode(
203            'text',
204            attrs={
205                'h_align': 'center',
206                'v_align': 'top',
207                'scale': 1.5 * scale,
208                'flatness': 1.0,
209                'host_only': True,
210                'shadow': 1.0,
211                'maxwidth': maxw,
212                'position': (pos[0], pos[1] - offs5),
213                'color': clr,
214            },
215        )
216        pos = pickup_pos
217        clr = (1, 0.8, 0.3) if ouya else (0.8, 0.5, 1)
218        self._pickup_image = ba.newnode(
219            'image',
220            attrs={
221                'texture': ba.gettexture('buttonPickUp'),
222                'absolute_scale': True,
223                'host_only': True,
224                'vr_depth': 10,
225                'position': pos,
226                'scale': (image_size, image_size),
227                'color': clr,
228            },
229        )
230        self._pick_up_text = ba.newnode(
231            'text',
232            attrs={
233                'v_align': 'top',
234                'h_align': 'center',
235                'scale': 1.5 * scale,
236                'flatness': 1.0,
237                'host_only': True,
238                'shadow': 1.0,
239                'maxwidth': maxw,
240                'position': (pos[0], pos[1] - offs5),
241                'color': clr,
242            },
243        )
244        clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0)
245        self._run_text_pos_top = (position[0], position[1] - 135.0 * scale)
246        self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale)
247        sval = 1.0 * scale if ba.app.vr_mode else 0.8 * scale
248        self._run_text = ba.newnode(
249            'text',
250            attrs={
251                'scale': sval,
252                'host_only': True,
253                'shadow': 1.0 if ba.app.vr_mode else 0.5,
254                'flatness': 1.0,
255                'maxwidth': 380,
256                'v_align': 'top',
257                'h_align': 'center',
258                'color': clr,
259            },
260        )
261        clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7)
262        self._extra_text = ba.newnode(
263            'text',
264            attrs={
265                'scale': 0.8 * scale,
266                'host_only': True,
267                'shadow': 0.5,
268                'flatness': 1.0,
269                'maxwidth': 380,
270                'v_align': 'top',
271                'h_align': 'center',
272                'color': clr,
273            },
274        )
275
276        if extra_pos_1 is not None:
277            self._extra_image_1: ba.Node | None = ba.newnode(
278                'image',
279                attrs={
280                    'texture': ba.gettexture('nub'),
281                    'absolute_scale': True,
282                    'host_only': True,
283                    'vr_depth': 10,
284                    'position': extra_pos_1,
285                    'scale': (image_size, image_size),
286                    'color': (0.5, 0.5, 0.5),
287                },
288            )
289        else:
290            self._extra_image_1 = None
291        if extra_pos_2 is not None:
292            self._extra_image_2: ba.Node | None = ba.newnode(
293                'image',
294                attrs={
295                    'texture': ba.gettexture('nub'),
296                    'absolute_scale': True,
297                    'host_only': True,
298                    'vr_depth': 10,
299                    'position': extra_pos_2,
300                    'scale': (image_size, image_size),
301                    'color': (0.5, 0.5, 0.5),
302                },
303            )
304        else:
305            self._extra_image_2 = None
306
307        self._nodes = [
308            self._bomb_image,
309            self._bomb_text,
310            self._punch_image,
311            self._punch_text,
312            self._jump_image,
313            self._jump_text,
314            self._pickup_image,
315            self._pick_up_text,
316            self._run_text,
317            self._extra_text,
318        ]
319        if show_title:
320            assert self._title_text
321            self._nodes.append(self._title_text)
322        if self._extra_image_1 is not None:
323            self._nodes.append(self._extra_image_1)
324        if self._extra_image_2 is not None:
325            self._nodes.append(self._extra_image_2)
326
327        # Start everything invisible.
328        for node in self._nodes:
329            node.opacity = 0.0
330
331        # Don't do anything until our delay has passed.
332        ba.timer(delay, ba.WeakCall(self._start_updating))
333
334    @staticmethod
335    def _meaningful_button_name(device: ba.InputDevice, button: int) -> str:
336        """Return a flattened string button name; empty for non-meaningful."""
337        if not device.has_meaningful_button_names:
338            return ''
339        return device.get_button_name(button).evaluate()
340
341    def _start_updating(self) -> None:
342
343        # Ok, our delay has passed. Now lets periodically see if we can fade
344        # in (if a touch-screen is present we only want to show up if gamepads
345        # are connected, etc).
346        # Also set up a timer so if we haven't faded in by the end of our
347        # duration, abort.
348        if self._lifespan is not None:
349            self._cancel_timer = ba.Timer(
350                self._lifespan,
351                ba.WeakCall(self.handlemessage, ba.DieMessage(immediate=True)),
352            )
353        self._fade_in_timer = ba.Timer(
354            1.0, ba.WeakCall(self._check_fade_in), repeat=True
355        )
356        self._check_fade_in()  # Do one check immediately.
357
358    def _check_fade_in(self) -> None:
359        from ba.internal import get_device_value
360
361        # If we have a touchscreen, we only fade in if we have a player with
362        # an input device that is *not* the touchscreen.
363        # (otherwise it is confusing to see the touchscreen buttons right
364        # next to our display buttons)
365        touchscreen: ba.InputDevice | None = ba.internal.getinputdevice(
366            'TouchScreen', '#1', doraise=False
367        )
368
369        if touchscreen is not None:
370            # We look at the session's players; not the activity's.
371            # We want to get ones who are still in the process of
372            # selecting a character, etc.
373            input_devices = [
374                p.inputdevice for p in ba.getsession().sessionplayers
375            ]
376            input_devices = [
377                i for i in input_devices if i and i is not touchscreen
378            ]
379            fade_in = False
380            if input_devices:
381                # Only count this one if it has non-empty button names
382                # (filters out wiimotes, the remote-app, etc).
383                for device in input_devices:
384                    for name in (
385                        'buttonPunch',
386                        'buttonJump',
387                        'buttonBomb',
388                        'buttonPickUp',
389                    ):
390                        if (
391                            self._meaningful_button_name(
392                                device, get_device_value(device, name)
393                            )
394                            != ''
395                        ):
396                            fade_in = True
397                            break
398                    if fade_in:
399                        break  # No need to keep looking.
400        else:
401            # No touch-screen; fade in immediately.
402            fade_in = True
403        if fade_in:
404            self._cancel_timer = None  # Didn't need this.
405            self._fade_in_timer = None  # Done with this.
406            self._fade_in()
407
408    def _fade_in(self) -> None:
409        for node in self._nodes:
410            ba.animate(node, 'opacity', {0: 0.0, 2.0: 1.0})
411
412        # If we were given a lifespan, transition out after it.
413        if self._lifespan is not None:
414            ba.timer(
415                self._lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage())
416            )
417        self._update()
418        self._update_timer = ba.Timer(
419            1.0, ba.WeakCall(self._update), repeat=True
420        )
421
422    def _update(self) -> None:
423        # pylint: disable=too-many-statements
424        # pylint: disable=too-many-branches
425        # pylint: disable=too-many-locals
426        from ba.internal import get_device_value, get_remote_app_name
427
428        if self._dead:
429            return
430        punch_button_names = set()
431        jump_button_names = set()
432        pickup_button_names = set()
433        bomb_button_names = set()
434
435        # We look at the session's players; not the activity's - we want to
436        # get ones who are still in the process of selecting a character, etc.
437        input_devices = [p.inputdevice for p in ba.getsession().sessionplayers]
438        input_devices = [i for i in input_devices if i]
439
440        # If there's no players with input devices yet, try to default to
441        # showing keyboard controls.
442        if not input_devices:
443            kbd = ba.internal.getinputdevice('Keyboard', '#1', doraise=False)
444            if kbd is not None:
445                input_devices.append(kbd)
446
447        # We word things specially if we have nothing but keyboards.
448        all_keyboards = input_devices and all(
449            i.name == 'Keyboard' for i in input_devices
450        )
451        only_remote = len(input_devices) == 1 and all(
452            i.name == 'Amazon Fire TV Remote' for i in input_devices
453        )
454
455        right_button_names = set()
456        left_button_names = set()
457        up_button_names = set()
458        down_button_names = set()
459
460        # For each player in the game with an input device,
461        # get the name of the button for each of these 4 actions.
462        # If any of them are uniform across all devices, display the name.
463        for device in input_devices:
464            # We only care about movement buttons in the case of keyboards.
465            if all_keyboards:
466                right_button_names.add(
467                    device.get_button_name(
468                        get_device_value(device, 'buttonRight')
469                    )
470                )
471                left_button_names.add(
472                    device.get_button_name(
473                        get_device_value(device, 'buttonLeft')
474                    )
475                )
476                down_button_names.add(
477                    device.get_button_name(
478                        get_device_value(device, 'buttonDown')
479                    )
480                )
481                up_button_names.add(
482                    device.get_button_name(get_device_value(device, 'buttonUp'))
483                )
484
485            # Ignore empty values; things like the remote app or
486            # wiimotes can return these.
487            bname = self._meaningful_button_name(
488                device, get_device_value(device, 'buttonPunch')
489            )
490            if bname != '':
491                punch_button_names.add(bname)
492            bname = self._meaningful_button_name(
493                device, get_device_value(device, 'buttonJump')
494            )
495            if bname != '':
496                jump_button_names.add(bname)
497            bname = self._meaningful_button_name(
498                device, get_device_value(device, 'buttonBomb')
499            )
500            if bname != '':
501                bomb_button_names.add(bname)
502            bname = self._meaningful_button_name(
503                device, get_device_value(device, 'buttonPickUp')
504            )
505            if bname != '':
506                pickup_button_names.add(bname)
507
508        # If we have no values yet, we may want to throw out some sane
509        # defaults.
510        if all(
511            not lst
512            for lst in (
513                punch_button_names,
514                jump_button_names,
515                bomb_button_names,
516                pickup_button_names,
517            )
518        ):
519            # Otherwise on android show standard buttons.
520            if ba.app.platform == 'android':
521                punch_button_names.add('X')
522                jump_button_names.add('A')
523                bomb_button_names.add('B')
524                pickup_button_names.add('Y')
525
526        run_text = ba.Lstr(
527            value='${R}: ${B}',
528            subs=[
529                ('${R}', ba.Lstr(resource='runText')),
530                (
531                    '${B}',
532                    ba.Lstr(
533                        resource='holdAnyKeyText'
534                        if all_keyboards
535                        else 'holdAnyButtonText'
536                    ),
537                ),
538            ],
539        )
540
541        # If we're all keyboards, lets show move keys too.
542        if (
543            all_keyboards
544            and len(up_button_names) == 1
545            and len(down_button_names) == 1
546            and len(left_button_names) == 1
547            and len(right_button_names) == 1
548        ):
549            up_text = list(up_button_names)[0]
550            down_text = list(down_button_names)[0]
551            left_text = list(left_button_names)[0]
552            right_text = list(right_button_names)[0]
553            run_text = ba.Lstr(
554                value='${M}: ${U}, ${L}, ${D}, ${R}\n${RUN}',
555                subs=[
556                    ('${M}', ba.Lstr(resource='moveText')),
557                    ('${U}', up_text),
558                    ('${L}', left_text),
559                    ('${D}', down_text),
560                    ('${R}', right_text),
561                    ('${RUN}', run_text),
562                ],
563            )
564
565        if self._force_hide_button_names:
566            jump_button_names.clear()
567            punch_button_names.clear()
568            bomb_button_names.clear()
569            pickup_button_names.clear()
570
571        self._run_text.text = run_text
572        w_text: ba.Lstr | str
573        if only_remote and self._lifespan is None:
574            w_text = ba.Lstr(
575                resource='fireTVRemoteWarningText',
576                subs=[('${REMOTE_APP_NAME}', get_remote_app_name())],
577            )
578        else:
579            w_text = ''
580        self._extra_text.text = w_text
581        if len(punch_button_names) == 1:
582            self._punch_text.text = list(punch_button_names)[0]
583        else:
584            self._punch_text.text = ''
585
586        if len(jump_button_names) == 1:
587            tval = list(jump_button_names)[0]
588        else:
589            tval = ''
590        self._jump_text.text = tval
591        if tval == '':
592            self._run_text.position = self._run_text_pos_top
593            self._extra_text.position = (
594                self._run_text_pos_top[0],
595                self._run_text_pos_top[1] - 50,
596            )
597        else:
598            self._run_text.position = self._run_text_pos_bottom
599            self._extra_text.position = (
600                self._run_text_pos_bottom[0],
601                self._run_text_pos_bottom[1] - 50,
602            )
603        if len(bomb_button_names) == 1:
604            self._bomb_text.text = list(bomb_button_names)[0]
605        else:
606            self._bomb_text.text = ''
607
608        # Also move our title up/down depending on if this is shown.
609        if len(pickup_button_names) == 1:
610            self._pick_up_text.text = list(pickup_button_names)[0]
611            if self._title_text is not None:
612                self._title_text.position = self._title_text_pos_top
613        else:
614            self._pick_up_text.text = ''
615            if self._title_text is not None:
616                self._title_text.position = self._title_text_pos_bottom
617
618    def _die(self) -> None:
619        for node in self._nodes:
620            node.delete()
621        self._nodes = []
622        self._update_timer = None
623        self._dead = True
624
625    def exists(self) -> bool:
626        return not self._dead
627
628    def handlemessage(self, msg: Any) -> Any:
629        assert not self.expired
630        if isinstance(msg, ba.DieMessage):
631            if msg.immediate:
632                self._die()
633            else:
634                # If they don't need immediate,
635                # fade out our nodes and die later.
636                for node in self._nodes:
637                    ba.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0})
638                ba.timer(3.1, ba.WeakCall(self._die))
639            return None
640        return super().handlemessage(msg)

A screen overlay of game controls.

category: Gameplay Classes

Shows button mappings based on what controllers are connected. Handy to show at the start of a series or whenever there might be newbies watching.

ControlsGuide( position: tuple[float, float] = (390.0, 120.0), scale: float = 1.0, delay: float = 0.0, lifespan: float | None = None, bright: bool = False)
 27    def __init__(
 28        self,
 29        position: tuple[float, float] = (390.0, 120.0),
 30        scale: float = 1.0,
 31        delay: float = 0.0,
 32        lifespan: float | None = None,
 33        bright: bool = False,
 34    ):
 35        """Instantiate an overlay.
 36
 37        delay: is the time in seconds before the overlay fades in.
 38
 39        lifespan: if not None, the overlay will fade back out and die after
 40                  that long (in milliseconds).
 41
 42        bright: if True, brighter colors will be used; handy when showing
 43                over gameplay but may be too bright for join-screens, etc.
 44        """
 45        # pylint: disable=too-many-statements
 46        # pylint: disable=too-many-locals
 47        super().__init__()
 48        show_title = True
 49        scale *= 0.75
 50        image_size = 90.0 * scale
 51        offs = 74.0 * scale
 52        offs5 = 43.0 * scale
 53        ouya = False
 54        maxw = 50
 55        self._lifespan = lifespan
 56        self._dead = False
 57        self._bright = bright
 58        self._cancel_timer: ba.Timer | None = None
 59        self._fade_in_timer: ba.Timer | None = None
 60        self._update_timer: ba.Timer | None = None
 61        self._title_text: ba.Node | None
 62        clr: Sequence[float]
 63        extra_pos_1: tuple[float, float] | None
 64        extra_pos_2: tuple[float, float] | None
 65        if ba.app.iircade_mode:
 66            xtweak = 0.2
 67            ytweak = 0.2
 68            jump_pos = (
 69                position[0] + offs * (-1.2 + xtweak),
 70                position[1] + offs * (0.1 + ytweak),
 71            )
 72            bomb_pos = (
 73                position[0] + offs * (0.0 + xtweak),
 74                position[1] + offs * (0.5 + ytweak),
 75            )
 76            punch_pos = (
 77                position[0] + offs * (1.2 + xtweak),
 78                position[1] + offs * (0.5 + ytweak),
 79            )
 80
 81            pickup_pos = (
 82                position[0] + offs * (-1.4 + xtweak),
 83                position[1] + offs * (-1.2 + ytweak),
 84            )
 85            extra_pos_1 = (
 86                position[0] + offs * (-0.2 + xtweak),
 87                position[1] + offs * (-0.8 + ytweak),
 88            )
 89            extra_pos_2 = (
 90                position[0] + offs * (1.0 + xtweak),
 91                position[1] + offs * (-0.8 + ytweak),
 92            )
 93            self._force_hide_button_names = True
 94        else:
 95            punch_pos = (position[0] - offs * 1.1, position[1])
 96            jump_pos = (position[0], position[1] - offs)
 97            bomb_pos = (position[0] + offs * 1.1, position[1])
 98            pickup_pos = (position[0], position[1] + offs)
 99            extra_pos_1 = None
100            extra_pos_2 = None
101            self._force_hide_button_names = False
102
103        if show_title:
104            self._title_text_pos_top = (
105                position[0],
106                position[1] + 139.0 * scale,
107            )
108            self._title_text_pos_bottom = (
109                position[0],
110                position[1] + 139.0 * scale,
111            )
112            clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7)
113            tval = ba.Lstr(
114                value='${A}:', subs=[('${A}', ba.Lstr(resource='controlsText'))]
115            )
116            self._title_text = ba.newnode(
117                'text',
118                attrs={
119                    'text': tval,
120                    'host_only': True,
121                    'scale': 1.1 * scale,
122                    'shadow': 0.5,
123                    'flatness': 1.0,
124                    'maxwidth': 480,
125                    'v_align': 'center',
126                    'h_align': 'center',
127                    'color': clr,
128                },
129            )
130        else:
131            self._title_text = None
132        pos = jump_pos
133        clr = (0.4, 1, 0.4)
134        self._jump_image = ba.newnode(
135            'image',
136            attrs={
137                'texture': ba.gettexture('buttonJump'),
138                'absolute_scale': True,
139                'host_only': True,
140                'vr_depth': 10,
141                'position': pos,
142                'scale': (image_size, image_size),
143                'color': clr,
144            },
145        )
146        self._jump_text = ba.newnode(
147            'text',
148            attrs={
149                'v_align': 'top',
150                'h_align': 'center',
151                'scale': 1.5 * scale,
152                'flatness': 1.0,
153                'host_only': True,
154                'shadow': 1.0,
155                'maxwidth': maxw,
156                'position': (pos[0], pos[1] - offs5),
157                'color': clr,
158            },
159        )
160        clr = (0.2, 0.6, 1) if ouya else (1, 0.7, 0.3)
161        pos = punch_pos
162        self._punch_image = ba.newnode(
163            'image',
164            attrs={
165                'texture': ba.gettexture('buttonPunch'),
166                'absolute_scale': True,
167                'host_only': True,
168                'vr_depth': 10,
169                'position': pos,
170                'scale': (image_size, image_size),
171                'color': clr,
172            },
173        )
174        self._punch_text = ba.newnode(
175            'text',
176            attrs={
177                'v_align': 'top',
178                'h_align': 'center',
179                'scale': 1.5 * scale,
180                'flatness': 1.0,
181                'host_only': True,
182                'shadow': 1.0,
183                'maxwidth': maxw,
184                'position': (pos[0], pos[1] - offs5),
185                'color': clr,
186            },
187        )
188        pos = bomb_pos
189        clr = (1, 0.3, 0.3)
190        self._bomb_image = ba.newnode(
191            'image',
192            attrs={
193                'texture': ba.gettexture('buttonBomb'),
194                'absolute_scale': True,
195                'host_only': True,
196                'vr_depth': 10,
197                'position': pos,
198                'scale': (image_size, image_size),
199                'color': clr,
200            },
201        )
202        self._bomb_text = ba.newnode(
203            'text',
204            attrs={
205                'h_align': 'center',
206                'v_align': 'top',
207                'scale': 1.5 * scale,
208                'flatness': 1.0,
209                'host_only': True,
210                'shadow': 1.0,
211                'maxwidth': maxw,
212                'position': (pos[0], pos[1] - offs5),
213                'color': clr,
214            },
215        )
216        pos = pickup_pos
217        clr = (1, 0.8, 0.3) if ouya else (0.8, 0.5, 1)
218        self._pickup_image = ba.newnode(
219            'image',
220            attrs={
221                'texture': ba.gettexture('buttonPickUp'),
222                'absolute_scale': True,
223                'host_only': True,
224                'vr_depth': 10,
225                'position': pos,
226                'scale': (image_size, image_size),
227                'color': clr,
228            },
229        )
230        self._pick_up_text = ba.newnode(
231            'text',
232            attrs={
233                'v_align': 'top',
234                'h_align': 'center',
235                'scale': 1.5 * scale,
236                'flatness': 1.0,
237                'host_only': True,
238                'shadow': 1.0,
239                'maxwidth': maxw,
240                'position': (pos[0], pos[1] - offs5),
241                'color': clr,
242            },
243        )
244        clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0)
245        self._run_text_pos_top = (position[0], position[1] - 135.0 * scale)
246        self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale)
247        sval = 1.0 * scale if ba.app.vr_mode else 0.8 * scale
248        self._run_text = ba.newnode(
249            'text',
250            attrs={
251                'scale': sval,
252                'host_only': True,
253                'shadow': 1.0 if ba.app.vr_mode else 0.5,
254                'flatness': 1.0,
255                'maxwidth': 380,
256                'v_align': 'top',
257                'h_align': 'center',
258                'color': clr,
259            },
260        )
261        clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7)
262        self._extra_text = ba.newnode(
263            'text',
264            attrs={
265                'scale': 0.8 * scale,
266                'host_only': True,
267                'shadow': 0.5,
268                'flatness': 1.0,
269                'maxwidth': 380,
270                'v_align': 'top',
271                'h_align': 'center',
272                'color': clr,
273            },
274        )
275
276        if extra_pos_1 is not None:
277            self._extra_image_1: ba.Node | None = ba.newnode(
278                'image',
279                attrs={
280                    'texture': ba.gettexture('nub'),
281                    'absolute_scale': True,
282                    'host_only': True,
283                    'vr_depth': 10,
284                    'position': extra_pos_1,
285                    'scale': (image_size, image_size),
286                    'color': (0.5, 0.5, 0.5),
287                },
288            )
289        else:
290            self._extra_image_1 = None
291        if extra_pos_2 is not None:
292            self._extra_image_2: ba.Node | None = ba.newnode(
293                'image',
294                attrs={
295                    'texture': ba.gettexture('nub'),
296                    'absolute_scale': True,
297                    'host_only': True,
298                    'vr_depth': 10,
299                    'position': extra_pos_2,
300                    'scale': (image_size, image_size),
301                    'color': (0.5, 0.5, 0.5),
302                },
303            )
304        else:
305            self._extra_image_2 = None
306
307        self._nodes = [
308            self._bomb_image,
309            self._bomb_text,
310            self._punch_image,
311            self._punch_text,
312            self._jump_image,
313            self._jump_text,
314            self._pickup_image,
315            self._pick_up_text,
316            self._run_text,
317            self._extra_text,
318        ]
319        if show_title:
320            assert self._title_text
321            self._nodes.append(self._title_text)
322        if self._extra_image_1 is not None:
323            self._nodes.append(self._extra_image_1)
324        if self._extra_image_2 is not None:
325            self._nodes.append(self._extra_image_2)
326
327        # Start everything invisible.
328        for node in self._nodes:
329            node.opacity = 0.0
330
331        # Don't do anything until our delay has passed.
332        ba.timer(delay, ba.WeakCall(self._start_updating))

Instantiate an overlay.

delay: is the time in seconds before the overlay fades in.

lifespan: if not None, the overlay will fade back out and die after that long (in milliseconds).

bright: if True, brighter colors will be used; handy when showing over gameplay but may be too bright for join-screens, etc.

def exists(self) -> bool:
625    def exists(self) -> bool:
626        return not self._dead

Returns whether the Actor is still present in a meaningful way.

Note that a dying character should still return True here as long as their corpse is visible; this is about presence, not being 'alive' (see ba.Actor.is_alive() for that).

If this returns False, it is assumed the Actor can be completely deleted without affecting the game; this call is often used when pruning lists of Actors, such as with ba.Actor.autoretain()

The default implementation of this method always return True.

Note that the boolean operator for the Actor class calls this method, so a simple "if myactor" test will conveniently do the right thing even if myactor is set to None.

def handlemessage(self, msg: Any) -> Any:
628    def handlemessage(self, msg: Any) -> Any:
629        assert not self.expired
630        if isinstance(msg, ba.DieMessage):
631            if msg.immediate:
632                self._die()
633            else:
634                # If they don't need immediate,
635                # fade out our nodes and die later.
636                for node in self._nodes:
637                    ba.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0})
638                ba.timer(3.1, ba.WeakCall(self._die))
639            return None
640        return super().handlemessage(msg)

General message handling; can be passed any message object.

Inherited Members
ba._actor.Actor
autoretain
on_expire
expired
is_alive
activity
getactivity