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