[Unity demo] Use unity to make a shooting game demo (Part 1)

1. Configure the vs code development environment

The main thing is to install the corresponding version of unity and configure the corresponding ide. Currently, I am using unity 2021.3.21.
Through the edit-prefreneces panel, configure the ide environment in the external tools option, and automatically use vs code to open the code document in the project.
insert image description here
insert image description here

2. Game Documentation

That is, the Game Design Document (GDD), we need to design the complete demo to be implemented in advance, including 5 parts:

  • Concept:
    A demo that can shoot from a third-person perspective by avoiding patrolling and vigilant enemies in the scene.

  • mechanism:

    1) The enemy will patrol along the designated route in the scene, and there is a vigilance range. When the protagonist enters the vigilance range of the enemy, the enemy will automatically change the patrol route and move towards the protagonist. 2) After the enemy touches the protagonist, it will deduct the protagonist
    's HP
    3) The protagonist can defend against approaching enemies by shooting, and can pick up items

  • User interface:

    1) Use WSAD to control the movement of the character, the mouse to control the direction of the camera, and use the left button to shoot
    2) The character picks up objects through contact
    3) Simple HUD (Head Up Display), showing the player's blood volume and remaining bullets

  • Plot:

    demo has no plot yet

  • Expressive style:

    Use the default 3D model of unity to build, without using custom shaders and textures as additional materials

3. Build levels

3.1. Create a basic scene

We use the default plane and cube to build a basic site model by scaling.
insert image description here

insert image description here
In order to facilitate the management of objects in the scene, we create an empty object named Special Management Environment, and drag all the components that make up the scene into it.
insert image description here

3.2. Creating prefabs

Next, we will place four bunkers to block enemies and provide shooting windows at the four corners of the scene.
We also use the basic model of unity to build these four bunkers, but assuming that the four bunkers have exactly the same structure, we have to repeat the same building operation four times, which is a bit too torture.
Here we use unity's pregab mechanism to create prefabs through empty objects and save them, so that when we need to build the same bunker next time, we can directly use the saved prefabs.
insert image description here
Drag the top-level empty object into the folder to create a ready-to-use prefab.
insert image description here
insert image description here
Create four shelters using prefabs.
insert image description here
Change one of the instances, apply it to the prefab, and see all prefab instances update in real time.
insert image description here
insert image description here
Add four slopes and cube platforms in the center of the scene, and add the first pickup in the lower right corner. The basic scene is built.
insert image description here

3.3. Make model animation

In the previous step we added a capsule as a pickup, and now we want this pickup to be able to animate in the scene.
Through window-animation-animation, open the animation panel and fix it next to the console panel.
insert image description here
Select the object to be animated and create animation.
insert image description here
Insert 5 keyframes that control the rotation, and these four keyframes only change the rotation.
insert image description here
insert image description here
It can be seen that when returning to the initial position after one rotation and performing the second cycle, the rotation pauses a bit, so we switch to the curve mode to check the action status.
It can be seen that our rate of change in segment 1 is not as stable as that in segment 2, so we have to re-insert frames to adjust.
insert image description here
insert image description here
insert image description here
We now have a rotating capsule in our scene.
insert image description here

3.4. Creating Particle Effects

Create a particle system object and adjust its position to be consistent with the capsule body.
insert image description here
insert image description here
insert image description here
insert image description here

4. Establish the basic control functions of the character

First, we create a light blue capsule representing the character controlled by the player, hang the rigidbody component, and set the rotation constraint of the xy axis in the rigidbody component.
Through the rigidbody component, we can make the capsule body hanging on it interact with the physics system of unity.
insert image description here
There are three common ways to move our game object:

  1. Move and rotate by adjusting the value of transform
  2. Use the rigidbody component to add force to the game object
  3. Use ready-made components directly, such as character controller and FirstPersonController

4.1. Modify transform to realize player character movement

First of all, we need to obtain the player's input. If you need to customize the buttons to control the movement direction, you need to complete the simple input configuration through the input manager panel.
Here we use the absolutely classic wasd movement model in fps (this is different from the cognition, ws is forward and backward movement, ad is rotation, which has been configured by default in unity), and endowed with a camera that moves with the player, Control the orientation of the follow camera by pointing the mouse.
insert image description here
In the Input manager panel, we can see that unity has configured input information by default.
Taking the horizontal axis as an example, when we query the corresponding input axis in c# (query the input of a specific axis through the Input.GetAxis() method, rather than query whether the corresponding button is pressed):

  • If the left button or A is pressed, it will return -1
  • If the right button or D is pressed, it will return 1
  • Returns 0 if the corresponding button is not pressed

insert image description here
The script to control the player's movement through transform is as follows:
the logic is very simple, get the parameters of the corresponding axis, multiply it by the moving speed and Time.deltaTime representing the frame interval.
If the frame rate is directly multiplied, the difference in the number of frames caused by the device difference between the players will cause the difference in movement, so the interval between two frames is used here to erase the difference in the number of frames.

//class: PlayerBehavior
public float moveSpeed = 1.0f;
public float rotateSpeed = 60f;

private float vInput;
private float hInput;

// 初始化部分无需写入
void Start()
{
    
    
    
}

void Update()
{
    
    
    vInput = Input.GetAxis("Vertical") * moveSpeed;
    hInput = Input.GetAxis("Horizontal" ) * rotateSpeed;

    this.transform.Translate(Vector3.forward * vInput * Time.deltaTime);
    this.transform.Rotate(Vector3.up * hInput * Time.deltaTime);
}

4.1.1 The problem of the player character falling to the ground

We quickly discovered that there was a problem with the player character falling over and swinging easily during the test.
insert image description here
Obviously this is a problem related to the physical system. We checked the rigidbody component and found that the option to lock the rotation axis was wrong.
insert image description here
Modify it to freeze the rotation in the x and z axis directions, and the problem is solved.
insert image description here

4.2. Making the camera follow the player

Create a new script and attach it to the camera.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraBehavior : MonoBehaviour
{
    
    
	//camOffset:相对于玩家角色的位置,摄像机位置的偏移值
    public Vector3 camOffset = new Vector3(0f, 1.2f, -2.6f);
    private Transform target;
    
    void Start()
    {
    
    
        target = GameObject.Find("Player").transform;
    }

    // 注意lateupdate也是monoBehavior提供的默认方法,其更新频率与帧率一致,但更新顺序在update之后
    // 使用LateUpdate以确保在玩家更新位置后,摄像机能及时跟上
    void LateUpdate()
    {
    
    
        this.transform.position = target.TransformPoint(camOffset);

        this.transform.LookAt(target);
    }
}

4.3. Use the rigidbody component to move the player character

In unity's movement types, there are mainly two types:

  • Kinematic movement: that is, kinematic movement, non-physical movement. That is, this type of movement will not be affected by the physical mechanism of unity, and is generally used for bone control, rather than
    the position and rotation changes of the game object in the scene.
  • Non-kinematic motion: that is, physical motion, non-kinematic motion. This kind of movement is mainly achieved by applying force to the object mounted rigidbody, rather than modifying the transform.

In 4.1, we mainly implemented a movement mechanism that mixes transform and rigidbody, that is, a movement mechanism that mixes kinematic and non-kinematic. Unity does not recommend that we mix these two movement mechanisms.
Next, we use the method provided by rigidbody to realize the movement of the player character.

public class PlayerBehavior : MonoBehaviour
{
    
    
    public float moveSpeed = 1.0f;
    public float rotateSpeed = 60f;

    private float vInput;
    private float hInput;
    private Rigidbody _rb;
    // Start is called before the first frame update
    void Start()
    {
    
    
        _rb = GetComponent<Rigidbody>();
    }

    // Update is called once per frame
    void Update()
    {
    
    
        vInput = Input.GetAxis("Vertical") * moveSpeed;
        hInput = Input.GetAxis("Horizontal" ) * rotateSpeed;

        // this.transform.Translate(Vector3.forward * vInput * Time.deltaTime);
        // this.transform.Rotate(Vector3.up * hInput * Time.deltaTime);
    }
	
	//面向物理系统专门的update方法FixedUpdate,该方法独立于帧率
    void FixedUpdate()
    {
    
    
        Vector3 rotation = Vector3.up * hInput;
		
		//Time.fixedDeltaTime:用于求解两次fixedupdate之间的时间差
        Quaternion angleRot = Quaternion.Euler(rotation * Time.fixedDeltaTime);

        _rb.MovePosition(this.transform.position + this.transform.forward * vInput * Time.fixedDeltaTime);
        _rb.MoveRotation(_rb.rotation * angleRot);
    }
}

4.4. Setting up Contact Picking

In the application of the physical system, we found that in addition to the rigidbody component, the game object often includes a collider component.
The collider component specifies the collision body, physical material, center of gravity, radius, height and other information of the object.
insert image description here
When two objects used in the collider component collide, the onCollisionEnter message is sent at the same time.
The script to mount the pickup game object is as follows:

public class ItemBehavior : MonoBehaviour
{
    
    
	//当不启用isTrigger属性的物体相互碰撞时,unity会调用OnCollisionEnter方法,执行碰撞逻辑
    void OnCollisionEnter(Collision collision)
    {
    
    
        if(collision.gameObject.name == "Player")
        {
    
    
        	//注意此时我们将拾取物胶囊体和粒子效果一起挂载在一个父级空物体下
        	//所以这里执行的是对父级物体的删除
        	//我们无法直接通过monobehaviour类来获取父级,得通过transform来获取挂载对象的父级
            Destroy(this.transform.parent.gameObject);

            Debug.Log("Item collected!");
        }
    }
}

insert image description here
insert image description here

4.5. Adding the first sentinel enemy

Now we need to add the first enemy to the scene and let him act as a sentry.
Although this enemy can't actively move at present, it has a very important duty to find the player character that enters the detection range.

We just mentioned that the collider component has an isTrigger property:
when it is not enabled, the collider will perform normal collision detection, and objects that want to collide with each other between colliders will be bounced off (or interaction between volumes will occur), thereby changing the object's Motion state, and call the OnCollisionEnter method.
When it is turned on, the collider will become a disembodied state, and no volumetric interaction will occur after the collider collides, and the object can move according to its original state of motion. In addition, the messages sent will also change. Corresponding to the process from entering the collider to leaving the collider, the messages (methods) triggered in sequence are: OnTriggerEnter, OnTriggerStay, OnTriggerExit.
insert image description here
First, we create an enemy object in the scene, add a sphere collider to it, and start isTrigger, with radius set to 8. The
insert image description here
insert image description here
insert image description here
script to mount to the enemy entity is as follows:

public class EnemyBehavior : MonoBehaviour
{
    
    
    void OnTriggerEnter(Collider other)
    {
    
    
        if (other.name == "Player")
        {
    
    
            Debug.Log("Player found within guard range!");
        }
    }

    void OnTriggerStay(Collider other)
    {
    
    
        if (other.name == "Player")
        {
    
    
            Debug.Log("Player stay in guard range!");
        }
    }

    void OnTriggerExit(Collider other)
    {
    
    
        if (other.name == "Player")
        {
    
    
            Debug.Log("Player exit guard range!");
        }
    }
}

insert image description here
We can go a step further and let the enemy wake up 12 points directly when they feel the player is approaching, and the color is brightened by one brightness. When the player exits the warning range, the color of the enemy fades accordingly.

public class EnemyBehavior : MonoBehaviour
{
    
    
    public Color ActivateColor;

    private Color CustomColor;
    private Renderer render;
    
    void Start()
    {
    
    
        render = this.GetComponent<Renderer>();
        CustomColor = new Color(0.58f, 0.0f, 0.632f, 1f);
    }

    void OnTriggerEnter(Collider other)
    {
    
    
        if (other.name == "Player")
        {
    
    
            Debug.Log("Player found within guard range!");
            render.material.SetColor("_Color", ActivateColor);
        }
    }

    void OnTriggerStay(Collider other)
    {
    
    
        if (other.name == "Player")
        {
    
    
            Debug.Log("Player stay in guard range!");
        }
    }

    void OnTriggerExit(Collider other)
    {
    
    
        if (other.name == "Player")
        {
    
    
            Debug.Log("Player exit guard range!");
            render.material.SetColor("_Color", CustomColor);
        }
    }
}

insert image description here

4.6. Implement player jumping

Update the player control script as follows:

public class PlayerBehavior : MonoBehaviour
{
    
    
    public float moveSpeed = 1.0f;
    public float rotateSpeed = 60f;
    //新增跳跃速度public变量
    public float jumpVelocity = 5.0f;

    private float vInput;
    private float hInput;
    private Rigidbody _rb;
    
    void Start()
    {
    
    
        _rb = GetComponent<Rigidbody>();
    }

    
    void Update()
    {
    
    
        vInput = Input.GetAxis("Vertical") * moveSpeed;
        hInput = Input.GetAxis("Horizontal" ) * rotateSpeed;
    }

    void FixedUpdate()
    {
    
    
    	//跳跃控制逻辑
        if(Input.GetKeyDown(KeyCode.Space))
        {
    
    
            _rb.AddForce(Vector3.up * jumpVelocity, ForceMode.Impulse);
        }

        Vector3 rotation = Vector3.up * hInput;

        Quaternion angleRot = Quaternion.Euler(rotation * Time.fixedDeltaTime);

        _rb.MovePosition(this.transform.position + this.transform.forward * vInput * Time.fixedDeltaTime);
        _rb.MoveRotation(_rb.rotation * angleRot);
    }
}

Soon we discovered that jumping is possible, but the success of this jump is very rare. Why does this little bi (ke) cub (love) not listen to the command of the space at all?

insert image description here
Although I can't see the situation of the keyboard in the above picture, in fact, I have been pressing the space all the time.

Obviously this is because FixedUpdate is not executed in the order of each game frame like Update. So we have to adjust the logic here again, and move part of the logic for monitoring the space key to update.

public class PlayerBehavior : MonoBehaviour
{
    
    
    public float moveSpeed = 1.0f;
    public float rotateSpeed = 60f;
    public float jumpVelocity = 5.0f;

    private float vInput;
    private float hInput;
    private float JInput;
    
    private Rigidbody _rb;
    
    void Start()
    {
    
    
        _rb = GetComponent<Rigidbody>();
    }

    
    void Update()
    {
    
    
        vInput = Input.GetAxis("Vertical") * moveSpeed;
        hInput = Input.GetAxis("Horizontal" ) * rotateSpeed;
        if (Input.GetKeyDown(KeyCode.Space))
        {
    
    
            JInput = jumpVelocity;
            Debug.Log("jump jump jump!");
        }

    }

    void FixedUpdate()
    {
    
    
        _rb.AddForce(Vector3.up * JInput, ForceMode.Impulse);
        JInput = 0f;

        Vector3 rotation = Vector3.up * hInput;

        Quaternion angleRot = Quaternion.Euler(rotation * Time.fixedDeltaTime);

        _rb.MovePosition(this.transform.position + this.transform.forward * vInput * Time.fixedDeltaTime);
        _rb.MoveRotation(_rb.rotation * angleRot);
    }
}

insert image description here
But soon we discovered a new problem, that is, pressing the space continuously will achieve continuous jumping and spiraling to the sky.

insert image description here
The way to deal with it is very simple, we need to check whether the player character is on the ground, and then the next jump logic is allowed to be executed after landing.

4.6.1 Use mask layer to realize player landing check

Next we add an independent layer and assign the environment to this layer, we will use this layer as a mask layer to determine whether the player character touches the ground.

First, we randomly click on a game object entity to add a layer named Ground.

insert image description here

Make sure all children's owning layers are adjusted.

insert image description here
The updated character control script is as follows:

public class PlayerBehavior : MonoBehaviour
{
    
    
    public float moveSpeed = 1.0f;
    public float rotateSpeed = 60f;
    public float jumpVelocity = 5.0f;
	//新增两个public变量,一个指定离地距离判断精度,一个指定mask layer
    public float distanceToGround = 0.1f;
    public LayerMask groundLayer;

    private float vInput;
    private float hInput;
    private float JInput;
    
    private Rigidbody _rb;

    private CapsuleCollider _col;
    
    void Start()
    {
    
    
        _rb = GetComponent<Rigidbody>();
        _col = GetComponent<CapsuleCollider>();
    }

    
    void Update()
    {
    
    
        vInput = Input.GetAxis("Vertical") * moveSpeed;
        hInput = Input.GetAxis("Horizontal" ) * rotateSpeed;
        //判断条件更新,确保同时满足落地和空格触发,才会进行跳跃
        if (IsGrounded() && Input.GetKeyDown(KeyCode.Space))
        {
    
    
            JInput = jumpVelocity;
            Debug.Log("jump jump jump!");
        }

    }

    void FixedUpdate()
    {
    
    
        _rb.AddForce(Vector3.up * JInput, ForceMode.Impulse);
        JInput = 0f;

        Vector3 rotation = Vector3.up * hInput;

        Quaternion angleRot = Quaternion.Euler(rotation * Time.fixedDeltaTime);

        _rb.MovePosition(this.transform.position + this.transform.forward * vInput * Time.fixedDeltaTime);
        _rb.MoveRotation(_rb.rotation * angleRot);
    }
	
	//通过private方法判断胶囊体是否地面layer mask的距离在精度范围内,返回布尔值
    private bool IsGrounded()
    {
    
    
        Vector3 capsuleBottom = new Vector3(_col.bounds.center.x, _col.bounds.min.y, _col.bounds.center.z);
		
        bool grounded = Physics.CheckCapsule(_col.bounds.center, capsuleBottom, distanceToGround, groundLayer, QueryTriggerInteraction.Ignore);

        return grounded;
    }
}

So far, the problem of continuous jumping has been solved, and our character can no longer push forward.

4.7. Implement bullet launch

Since it is an FPS demo, the most critical part must be the S link. Next, we realize the shooting function through instantiation.

The logic is also very simple:
after the shooting is triggered, we will instantiate the entity of the bullet at the specified position and make it move in a fixed direction.

In terms of implementation, we use the Instantiate method and provide it with the prefab object of the bullet to generate the position and rotation.

First we create the prefab of the bullet entity, remember to add the rigidbody component.

insert image description here
The code of the player character control, the updated part is as follows:

public class PlayerBehavior : MonoBehaviour
{
    
    
    //其余定义部分不变

    public GameObject bullet;
    public float bulletSpeed = 100f;
    private bool bulletTrigger = false;
    
    void Start()
    {
    
    
        _rb = GetComponent<Rigidbody>();
        _col = GetComponent<CapsuleCollider>();
    }

    
    void Update()
    {
    
    
    	//同样的,获取鼠标输入的部分移入Update中
    	//通过bulletTrigger作为触发器,确保FixedUpdate帧能够获取到子弹发射的信号
        if(Input.GetMouseButtonDown(0)){
    
    
            bulletTrigger = true;
        }
        
        //其余部分不变
    }

    void FixedUpdate()
    {
    
    
        if(bulletTrigger)
        {
    
    
        	//使用预制件实例化子弹实体
            GameObject newBullet = Instantiate(bullet, this.transform.position + new Vector3(1, 0, 0), this.transform.rotation) as GameObject;

            Rigidbody bulletRB = newBullet.GetComponent<Rigidbody>();

            bulletRB.velocity = this.transform.forward * bulletSpeed;

            bulletTrigger = false;
        }

        //其余部分不变
    }

    private bool IsGrounded()
    {
    
    
        //不变
    }
}

insert image description here

4.7.1 Transform to first-person perspective

We can clearly see that the existence of the player character itself blocks the bullet. So we need to make some adjustments to the script that the camera follows: the
main changes are the offset and viewing direction of the camera, and the spawning position of the bullet.

The script of the camera is updated as follows:

public class CameraBehavior : MonoBehaviour
{
    
    
	//在inspector面板中配置偏移量,使摄像机一直在玩家角色前方 
    public Vector3 camOffset = new Vector3(0f, 1.0f, 1.0f);
    
    private Transform target;
    
    void Start()
    {
    
    
        target = GameObject.Find("Player").transform;
    }

    // 注意lateupdate也是monoBehavior提供的默认方法,其更新频率与帧率一致,但更新顺序在update之后
    void LateUpdate()
    {
    
    
        this.transform.position = target.TransformPoint(camOffset);

        // 这里更新为,观察当前朝向远处的一个位置
        this.transform.LookAt(target.position + target.transform.forward * 10);
    }
}

Also, we made some tweaks to the player character prefab, adding a capsule as a representation of the weapon.
insert image description here
This way there are no situations where the player character gets in the way of aiming and shooting.
insert image description here

4.7.2 Set the bullet to disappear automatically

Now, although the bullets and viewing angles are correct, there will be many bullet entities rolling around on the ground if you shoot randomly. On the one hand, it is easy to trigger collision events by mistake, and a large number of bullet entities will also occupy redundant memory and physical calculation consumption.
By setting the timer to disappear, we allow the system to automatically destroy the bullet entities that have been generated in the scene at regular intervals.

Attach a script to the bullet prefab: automatically destroy the generated entity according to the delay

public class BulletBehavior : MonoBehaviour
{
    
    
    public float onscreenDelay = 3f;

    void Start()
    {
    
    
        Destroy(this.gameObject, onscreenDelay);
    }
}

insert image description here
So far, the basic functions of the system have been realized.
By the time of this part, this blog post has been significantly too long, and the remaining parts:
such as the interaction between bullets and objects, HUD display, game manager, enemy AI, etc.
I will explain it in the next article.
[unity demo] Use unity to make a shooting game demo (below)

Guess you like

Origin blog.csdn.net/misaka12807/article/details/131860091