This is a project that was years in the making, I went through many iterations that failed one way or the other. However, the final project was only started about a few months ago. It's great to see this project realized at last! The trigger really works, it has the appropriate sound effects, it has two firing speeds, and a "virtual ammo" system that you replenish by physically removing and reinserting the cartridge. So, where did it all begin?
The initial step of the process was to design the 3D printable case, which I did in blender. I found someone who extracted a model of the Necrochasm from the game itself and went to work sculpting out the finer details, hollowing out the interior, and adding LED areas. I also took this time to log into Destiny, and use my in-game Necrochasm to record the proper sound effects.
I exported the resulting pieces from blender to fusion 360 where I added the hardware mounting features. My vision for this prop was to have one area where most of the electronics were stored. This trapezoidal area at the bottom-front of the prop looked like it had the most storage capacity.
At the same time the model was in development with blender and fusion 360, I was getting my electronic parts in order. I decided early on that I wanted to use the prop-maker rp2040 feather because it had features that I would normally use separate parts for, like sound output and lipo charging. Not only that, but I had limited python skills, and writing circuitpython code for the prop-maker would be an excellent opportunity. Instead of writing the full code right away, I wrote a test script that would ensure all of my hardware was working as I was putting together this prop.
When printing the pieces, I used two colors of filament: black and translucent gray. The reason why I used a translucent gray filament instead of printing in clear was because I wanted the led backlit pieces to be less noticeable when the prop was turned off. It's quite obvious a lot of black filament was used, the main body of the prop alone came out in sixteen pieces, eight for each half. I used a special superglue for repairing gaming miniatures to stick the pieces together, then used an epoxy putty called "green stuff" (Yes, that is the name.) to smooth out the resulting seams. Then it was time for painting.
Once all the parts were painted, I finally moved on to the electronics assembly. First order of business was getting everything on the main electronics platform sorted out. The prop-maker feather sat on a lid covering the lipo battery I planned to power this model with, and a DC rumble motor sat right next to the feather. I put on the electronics platform a ground bus bar, installed a mini slide switch grounding the EN pin, hooked up the speaker and wired a single button. The trickiest part of this whole setup was the H-bridge motor driver board. I thought that 3.3v would be enough to power it but I was wrong, so very wrong. Worse, when I tried to use the motor, it made the speaker let out an ear-piercing screech. I eventually hooked up the driver board to the 5V pin on the terminal block.
One of the things I'm most proud of is how discreetly I put in the battery monitoring circuit. Per Adafruit's instructions, I soldered two 100K ohm resistors in series connecting the battery voltage pin to an analog pin and then to ground. However, I did so in a way that they would be barely noticeable.
Next, I went to work on the main body of the prop. Basically, my plan was to have two symmetrical strips of neopixels lighting up both halves of the prop. However, there were some neopixels that sat in the middle of the prop, and there was also the removable cartridge to take care of. So, what I did was extend the strip on only one half of the case to account for these lights. The neopixels in the cartridge were the very last in the sequence, being the only removable aspect.
Once all the electronics were wired up, the next step was to glue both halves of the casing together. Admittedly, making all those neopixels and the trigger non-serviceable was a risky move, but I was going for immersion when it came to this prop. However, the electronics platform, where the prop-maker was located, and the magnetic connector for the cartridge would be still accessible. This was especially important, because I was nowhere near done with the prop-maker's code at this stage.
Now that all the hardware was working, it was time to program it. This was the biggest challenge of all. Animating the flame segments took a lot of the prop-maker's processing power, and it was messing up the timing of the code. After all, I had a few LEDs that were supposed to blink in sync with the firing sound effect. In the end, everything had to be subdued. I had to lower the brightness of the LEDs to give more power to the motor, the flame LEDs would stop animating when the Necrochasm was firing, but I think my biggest handicap of all was the fact this thing was written in python. I already knew how to program for the Arduino family of boards at this point, but I wanted to write the code for this in circuitpython as a learning experience, and I knew the prop-maker was Arduino compatible. Maybe I'll rewrite the code in the Arduino IDE at a later date, but I'm happy with how it turned out for now.
# Necrochasm Prop Maker Feather RP2040 Python Code # Written by Christopher Littlefield # "Is your Light bright enough to stand in full gaze of the Hive's abyss?" import board import analogio import digitalio import pwmio import time import random import neopixel import audiobusio import audiocore import audiomixer from adafruit_motor import motor from adafruit_debouncer import Debouncer max_ammo = 50 ammo = 0 rpm = 720 # The Necrochasm has two firing speeds: 720 and 900 rpm. # This value controls the timing of the code to make the prop's firing rate accurate. ammo_interval = 60 / rpm ammo_time = -3600 infinite_ammo = False infinite_ammo_timer = 5.0 # Desperation is an in-game feature where the Necrochasm's reload speed is temporailiy # increased to 900 RPM. In the prop, this is meant to be triggered by a button press, # then reloading the gun. desperation_ready = False desperation_activated = -3600.0 desperation_pressed = -3600.0 desperation_timer = 6.0 desperation_pin = digitalio.DigitalInOut(board.D12) desperation_pin.direction = digitalio.Direction.INPUT desperation_pin.switch_to_input(pull=digitalio.Pull.UP) desperation_button = Debouncer(desperation_pin) # The reload pin is grounded while the ammo cartridge is inserted into the prop. # If the pin is open, the cartridge has been removed. reload_pin = digitalio.DigitalInOut(board.D11) reload_pin.direction = digitalio.Direction.INPUT reload_pin.switch_to_input(pull=digitalio.Pull.UP) reload = Debouncer(reload_pin) trigger_pin = digitalio.DigitalInOut(board.EXTERNAL_BUTTON) trigger_pin.direction = digitalio.Direction.INPUT trigger_pin.switch_to_input(pull=digitalio.Pull.UP) trigger = Debouncer(trigger_pin) # This LED flashes in sync with the Necrochasm's firing rate. barrel_led = digitalio.DigitalInOut(board.D10) barrel_led.direction = digitalio.Direction.OUTPUT barrel_led.value = False rumble_a = pwmio.PWMOut(board.D24, frequency=50) rumble_b = pwmio.PWMOut(board.D25, frequency=50) rumble_motor = motor.DCMotor(rumble_a, rumble_b) throttle_motor = throttle720 = 0.17 throttle900 = 0.25 # Neopixel Indicies fiber_optics_indexA = 0 lower_flames_index = fiber_optics_indexA + 1 upper_flames_index = lower_flames_index + 13 targeting_light_index = upper_flames_index + 21 fiber_optics_indexB = targeting_light_index + 1 core_stripes_index = fiber_optics_indexB + 1 stripe_index = core_stripes_index + 3 core_index = stripe_index + 7 core_flames_index = core_index + 1 ridges_index = core_flames_index + 3 cartridge_index = ridges_index + 5 end_of_string = cartridge_index + 5 num_pixels = end_of_string + 1 # NOTE: "targeting_light_index" refers to a RGB LED, as opposed to the regular GRB leds. # So, switch red and green values when programming color. # Enable the external power pin so the addressable LEDS and sound can be used. enable = digitalio.DigitalInOut(board.EXTERNAL_POWER) enable.direction = digitalio.Direction.OUTPUT enable.value = True # These variables help measure the battery level of the Necrochasm. vbat_voltage = analogio.AnalogIn(board.A3) battery_voltage = 0.0 pixel = neopixel.NeoPixel(board.EXTERNAL_NEOPIXELS, num_pixels) pixel.brightness = 0.1 flame_start = 128 # Audio Related i2s = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA) fire_wav = necro720 = audiocore.WaveFile("sounds/Necro720.wav") necro900 = audiocore.WaveFile("sounds/Necro900.wav") headshot = audiocore.WaveFile("sounds/Headshot.wav") roar = audiocore.WaveFile("sounds/Roar.wav") # The reason for the mixer is in the event the headshot file is played over the # normal firing sound. mixer = audiomixer.Mixer(voice_count=2, sample_rate=necro720.sample_rate, channel_count=2, bits_per_sample=necro720.bits_per_sample, samples_signed=True) i2s.play(mixer) mixer.voice[0].level = 1 # voice 0 is used for firing the Necrochasm mixer.voice[1].level = 1 # voice 1 is used for playing the Headshot file def animate_flames(pixel_array, low, high): flame_increment = 40 flame_max_brightness = 255 for i in range(low, high): color = list(pixel_array[i]) # Neopixel colors are a tuple, so convert to list. # Pull Green value from index 1. color[1] += flame_increment if color[1] > flame_max_brightness: color[1] = random.randrange(0, int(flame_increment/2)) pixel_array[i] = (0, color[1], 0) def get_voltage(pin): return pin.value / 65535 * 3.3 * 2 # Startup Sequence if reload.value is False: ammo = max_ammo else: ammo = 0 battery_voltage = get_voltage(vbat_voltage) pixel[fiber_optics_indexA] = (0, 255, 0) for i in range(lower_flames_index, targeting_light_index): pixel[i] = (0, random.randrange(0, flame_start), 0) # Remember, we swap red and green at this index. pixel[targeting_light_index] = (255, 0, 0) for i in range(fiber_optics_indexB, core_flames_index): pixel[i] = (0, 255, 0) for i in range(core_flames_index, ridges_index): pixel[i] = (0, random.randrange(0, flame_start), 0) for i in range(ridges_index, end_of_string): pixel[i] = (0, 255, 0) while True: reload.update() desperation_button.update() trigger.update() #Battery Voltage Indicator battery_voltage = get_voltage(vbat_voltage) if battery_voltage > 3.8: pixel[stripe_index] = (0, 255, 0) else: pixel[stripe_index] = (0, 0, 0) if battery_voltage > 3.7: pixel[stripe_index+1] = (0, 255, 0) else: pixel[stripe_index+1] = (0, 0, 0) if battery_voltage > 3.6: pixel[stripe_index+2] = (0, 255, 0) else: pixel[stripe_index+2] = (0, 0, 0) if battery_voltage > 3.5: pixel[stripe_index+3] = (0, 255, 0) else: pixel[stripe_index+3] = (0, 0, 0) if battery_voltage > 3.4: pixel[stripe_index+4] = (0, 255, 0) else: pixel[stripe_index+4] = (0, 0, 0) if battery_voltage > 3.3: pixel[stripe_index+5] = (0, 255, 0) else: pixel[stripe_index+5] = (0, 0, 0) if battery_voltage > 3.2: pixel[stripe_index+6] = (0, 255, 0) else: pixel[stripe_index+6] = (0, 0, 0) if trigger.fell and ammo > 0: mixer.voice[0].play(fire_wav, loop=infinite_ammo ) rumble_motor.throttle = throttle_motor if trigger.rose or ammo <= 0: mixer.voice[0].stop() rumble_motor.throttle = 0 barrel_led.value = False for i in range(ridges_index, cartridge_index): pixel[i] = (0, 255, 0) if trigger.value is False and ammo > 0: if time.monotonic() > (ammo_time + (ammo_interval / 2)): if barrel_led.value is False: barrel_led.value = True for i in range(ridges_index, cartridge_index): pixel[i] = (0, 255, 0) ammo_time = time.monotonic() else: barrel_led.value = False for i in range(ridges_index, cartridge_index): pixel[i] = (0, 0, 0) ammo_time = time.monotonic() if infinite_ammo is False: ammo -= 1 if ammo > 25: topLED = (ammo - 25) * 10 pixel[cartridge_index] = pixel[cartridge_index + 2] = (0, 250, 0) pixel[cartridge_index + 1] = pixel[cartridge_index + 3] = (0, topLED, 0) else: pixel[cartridge_index] = pixel[cartridge_index + 2] = (0, ammo * 10, 0) pixel[cartridge_index + 1] = pixel[cartridge_index + 3] = (0, 0, 0) else: animate_flames(pixel, lower_flames_index, targeting_light_index) animate_flames(pixel, core_flames_index, ridges_index) if reload.fell: ammo = max_ammo for i in range(cartridge_index, cartridge_index + 4): pixel[i] = (0, 255, 0) if desperation_ready: desperation_activated = time.monotonic() rpm = 900 ammo_interval = 60 / rpm fire_wav = necro900 throttle_motor = throttle900 desperation_ready = False if reload.rose: ammo = 0 if desperation_button.fell: desperation_pressed = time.monotonic() if desperation_button.rose: if time.monotonic() > (desperation_pressed + infinite_ammo_timer): mixer.voice[1].play(roar, loop=False) infinite_ammo = not infinite_ammo ammo = max_ammo for i in range(cartridge_index, cartridge_index + 4): pixel[i] = (0, 255, 0) else: mixer.voice[1].play(headshot, loop=False) desperation_ready = True if time.monotonic() > (desperation_activated + desperation_timer) and rpm == 900: rpm = 720 ammo_interval = 60 / rpm fire_wav = necro720 throttle_motor = throttle720 if trigger.value is False: rumble_motor.throttle = throttle720 else: rumble_motor.throttle = 0 if mixer.voice[0].playing: mixer.voice[0].play(fire_wav, loop=True)
I ended up using some parts from Adafruit, but a lot of my parts came from Amazon. The parts I did get from Adafruit are as follows:
First, the brains of the Necrochasm itself: the Prop-Maker Feather.
The removable cartridge was accomplished thanks to a 5-pin magnetic connector.
Most of my LEDs were a generic addressable strip, but there were some exceptions.
A small button on my electronics platform was backed by a soft tactile switch.
And that is how I did it! This was less of an instruction manual and more of a description of the process. Hopefully you enjoyed seeing how I brought my prop to life.