In modern web development, the boundaries between classic and web applications are blurring every day. Today, we can create not only interactive websites but also full-fledged games right in the browser. One of the tools that makes this possible is the React Three Fiber library - a powerful tool for creating 3D graphics based on Three.js using React technology.
In today's article, we will implement:
Repository on
Before we start, let's create a new images folder for images in the assets folder. And move the image of the floor surface to this folder.
Also, change the path to the image in the Ground.jsx file.
As in most shooters, aiming is activated by right-clicking. But users can reassign this button to any other at any time in the game settings. Therefore, we will not set the condition of pressing this key directly in the code but will implement a separate config where we will set the control.
In React(Vite), it is already possible to create such a config without additional libraries. To do this, you need to create a .env file in the root of the project. Then, it is in the VITE_*** format that we can set environment variables that we can use anywhere in our project.
In the .env configuration file, let's add two variables that will contain the codes of mouse button presses. Namely, the code for the mouse button to activate firing and also for aiming.
Now, we need to rework the mouse button logic so that different actions are activated when different buttons are pressed.
But first, we need to fix some incorrect behavior when intercepting the mouse cursor on the canvas. At the moment, clicking the mouse on the screen immediately triggers a shot action, which looks a bit odd. So we'll add logic that until the cursor is intercepted by the application, then no click events will occur.
Now, we'll use the library to store the global state of Zustand. In the App.jsx file, we'll add the state and add event handlers for locking and unlocking the cursor.
In the Weapon.jsx file, we will add new logic that will differentiate between mouse keys pressed and also take into account the state of the intercepted cursor.
Let's create a function mouseButtonHandler. Inside, we will determine the current state of the intercepted cursor, and if the cursor has not been intercepted yet, we will not perform any actions. We also need to import from .env config the values for the mouse keys that activate the firing or aiming mode.
Let's also change the logic of the event handlers when pressing and releasing mouse keys.
Now, let's directly implement the aiming animation.
First, let's add a new state, useAimingStore, to store the aiming state.
Let's add a variable to be able to change the aiming state.
And in the mouseButtonHandler function, where we previously left empty space for the AIM_BUTTON button, we add a state change.
Let's go to the Player.jsx file and do the implementation inside it.
Firstly, we need to import the useAimingStore states from the Weapon.jsx file. Also, move the easing constant to the root of the file.
Let's add the isAiming state for further use in the file.
To save the state of the animation, let's add two states: aiming animation and returning to the original state.
Now, let's create the initAimingAnimation function, which will describe both states of the aiming animation.
In order for this animation to run, it is necessary to call the initAimingAnimation function when initializing the application. This is exactly when the object with the weapon model inside is ready for interaction.
When changing the isAiming state, it is necessary to trigger either the aiming animation or the weapon return to the initial position. For this purpose, let's add useEffect, within which one or another logic will be triggered according to the condition. So, for example, when starting the aiming process, it will be necessary to stop the "wiggle" animation and then start the aiming animation. When the mouse button is released, the animation of returning the weapon to the initial position is triggered, and when the onComplete event is triggered, the "wiggle" animation is re-run.
But now, when the application is initialized, the animation starts up, and it looks like the player was aiming and then exits the aiming mode. This happens because, by default, isAiming is set to false, and the "or" branch in the condition is immediately triggered during initialization. You can solve this by fixing the default value to null and then modifying the condition by adding a specific value to the condition.
So now we have an aiming animation when the right mouse button is clicked and an exit from this mode when it is released.
Before we get to the next part of our article, let's do some refactoring of the animation implementation.
In the Player.jsx file, let's change the function name from setAnimationParams to setSwayingAnimationParams. And also replace this function name in other places.
And from the initSwayingObjectAnimation function, we will remove the easing constant because earlier we moved it to the root of the file.
Let's move on to fixing the Weapon.jsx file.
Change the value forrecoilDuration to 50.
Let's remove recoilBackAnimation for unnecessary. Instead, let's now add isRecoilAnimationFinished.
For the generateNewPositionOfRecoil function, let's add a default value for the currentPosition parameter.
In the initRecoilAnimation function, let's remove the initialPosition constant for uselessness.
For Tween-animation, we will rework the logic, making it automatically-return to the initial position from which the animation started. We will also remove the return animation twRecoilBackAnimation.
Let's rework useEffect by splitting it into 2 different functions. The first will initialize the animation, and the second will run the weapon recoil animation.
Now, let's implement the flash display when the weapon is fired.
In the Weapon.jsx file import the useLoader function. And also, load the flash image into assets/images.
In the component, import this image as FlashShoot.
Next, let's use the useLoader function to load this image into the scene. We will also add a state to save the flashAnimation.
Let's create a new flashOpacity state to smoothly change the transparency of the flash. We will also create a new function initFlashAnimation, in which we will describe the animation sequence. And after that, we'll useEffect to initialize the animation.
Now, we need to display this image on the stage at the same level as the weapon model, setting the location so that the image is located directly on the end of the muzzle of the weapon.
And finally, let's add a call to the startShooting function for animation at each shot.
Let's add the prepared sound file to the assets/sounds folder.
Import it into a file.
Using HTMLAudioElement, we can add a sound file for the current page (not for the scene).
When the startShooting function is called, we launch the given audio file. For several shots, the audio files to be launched will start and overlap with each other. And at the end just stop playing.
In this article, we have added an aiming animation, a flash animation when shooting, and a sound effect when shooting. In the next article, we will continue to refine our game by adding new functionality.
Thanks for reading, and I will be glad to respond to your comments!
Also published here.