Unity3D之巡逻兵(Patrolman)小游戏制作

演示视频链接 优酷视频链接

Github项目文件传送门 Github项目文件传送门

  1. 设计要求
    • 游戏设计要求
      • 创建一个地图和若干巡逻兵;
      • 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
      • 巡逻兵碰撞到障碍物如墙体,则会自动选下一个点为目标;
      • 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
      • 失去玩家目标后,继续巡逻;
      • 计分:每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束
    • 程序设计要求
      • 必须使用订阅与发布模式传消息、工厂模式生产巡逻兵
  2. 类图(因为参考的博客是老师推荐的结构,所以没有轻易改动主要结构)
    这里写图片描述
  3. 详细代码分析

这个脚本是用来监测用户的键盘输入控制里面玩家角色的移动,可以上下左右移动
UserIntetrface.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;

public class UserInterface : MonoBehaviour {
    private IUserAction action;
    void Start () {
        action = SceneController.getInstance() as IUserAction;
    }

    void Update () {
        detectKeyInput();

    }

    void detectKeyInput() {
        if (Input.GetKey(KeyCode.UpArrow)) {
            action.heroMove(Diretion.UP);
        }
        if (Input.GetKey(KeyCode.DownArrow)) {
            action.heroMove(Diretion.DOWN);
        }
        if (Input.GetKey(KeyCode.LeftArrow)) {
            action.heroMove(Diretion.LEFT);
        }
        if (Input.GetKey(KeyCode.RightArrow)) {
            action.heroMove(Diretion.RIGHT);
        }
    }
}

这个脚本文件主要是处理游戏中出现的一些情况的处理,但是具体实现在GameModel里面,这只是起一个场景控制的作用
SceneController.cs

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

namespace Com.Patrols {
    public class Diretion {
        public const int UP = 0;
        public const int DOWN = 2;
        public const int LEFT = -1;
        public const int RIGHT = 1;
    }

    public class FenchLocation {
        public const float FenchHori = 12.42f;
        public const float FenchVertLeft = -3.0f;
        public const float FenchVertRight = 3.0f;
    }

    public interface IUserAction {
        void heroMove(int dir);
    }

    public interface IAddAction {
        void addRandomMovement(GameObject sourceObj, bool isActive);
        void addDirectMovement(GameObject sourceObj);
    }

    public interface IGameStatusOp {
        int getHeroStandOnArea();
        void heroEscapeAndScore();
        void patrolHitHeroAndGameover();
    }

    public class SceneController : System.Object, IUserAction, IAddAction, IGameStatusOp {
        private static SceneController instance;
        private GameModel myGameModel;
        private GameEventManager myGameEventManager;

        public static SceneController getInstance() {
            if (instance == null)
                instance = new SceneController();
            return instance;
        }

        internal void setGameModel(GameModel _myGameModel) {
            if (myGameModel == null) {
                myGameModel = _myGameModel;
            }
        }

        internal void setGameEventManager(GameEventManager _myGameEventManager) {
            if (myGameEventManager == null) {
                myGameEventManager = _myGameEventManager;
            }
        }

        public void heroMove(int dir) {
            myGameModel.heroMove(dir);
        }
        public void addRandomMovement(GameObject sourceObj, bool isActive) {
            myGameModel.addRandomMovement(sourceObj, isActive);
        }

        public void addDirectMovement(GameObject sourceObj) {
            myGameModel.addDirectMovement(sourceObj);
        }

        public int getHeroStandOnArea() {
            return myGameModel.getHeroStandOnArea();
        }

        public void heroEscapeAndScore() {
            myGameEventManager.heroEscapeAndScore();
        }

        public void patrolHitHeroAndGameover() {
            myGameEventManager.patrolHitHeroAndGameover();
        }
    }
}



类似于打飞碟游戏,这个脚本文件用来产生复制品的巡逻兵,以及确定他们可以走的方向集合
PatrolFactory.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;

namespace Com.Patrols {
    public class PatrolFactory : System.Object {
        private static PatrolFactory instance;
        private GameObject PatrolItem;

        private Vector3[] PatrolPosSet = new Vector3[] { new Vector3(-6, 0, 16), new Vector3(-1, 0, 19),
            new Vector3(6, 0, 16), new Vector3(-5, 0, 7), new Vector3(0, 0, 7), new Vector3(6, 0, 7)};

        public static PatrolFactory getInstance() {
            if (instance == null)
                instance = new PatrolFactory();
            return instance;
        }

        public void initItem(GameObject _PatrolItem) {
            PatrolItem = _PatrolItem;
        }

        public GameObject getPatrol() {
            GameObject newPatrol = Camera.Instantiate(PatrolItem);
            return newPatrol;
        }

        public Vector3[] getPosSet() {
            return PatrolPosSet;
        }
    }
}

这个就是具体的巡逻兵移动的脚本代码,每帧判断是否碰到玩家人物,碰到的话判断游戏结束,没有的话如果碰到墙体等障碍物就会改变方向
PatrolBehaviour.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;


public class PatrolBehaviour : MonoBehaviour {
    private IAddAction addAction;
    private IGameStatusOp gameStatusOp;

    public int ownIndex;
    public bool isCatching;

    private float CATCH_RADIUS = 3.0f;

    void Start () {
        addAction = SceneController.getInstance() as IAddAction;
        gameStatusOp = SceneController.getInstance() as IGameStatusOp;

        ownIndex = getOwnIndex();
        isCatching = false;
    }

    void Update () {
        checkNearByHero();
    }

    int getOwnIndex() {
        string name = this.gameObject.name;
        char cindex = name[name.Length - 1];
        int result = cindex - '0';
        return result;
    }

    void checkNearByHero () {
        if (gameStatusOp.getHeroStandOnArea() == ownIndex) { 
            if (!isCatching) {
                isCatching = true;
                addAction.addDirectMovement(this.gameObject);
            }
        }
        else {
            if (isCatching) { 
                gameStatusOp.heroEscapeAndScore();
                isCatching = false;
                addAction.addRandomMovement(this.gameObject, false);
            }
        }
    }

    void OnCollisionStay(Collision e) {
        if (e.gameObject.name.Contains("Patrol") || e.gameObject.name.Contains("fence")
            || e.gameObject.tag.Contains("FenceAround")) {
            isCatching = false;
            addAction.addRandomMovement(this.gameObject, false);
        }

        if (e.gameObject.name.Contains("Hero")) {
            gameStatusOp.patrolHitHeroAndGameover();
        }
    }
}

这个是玩家人物身上的脚本,人物身上有个所在区域的变量信息,每个巡逻兵时刻检测该信息,来判断是否在自己区域,从而有对应行动
HeroStatus.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;

public class HeroStatus : MonoBehaviour {
    public int standOnArea = -1;

    void Start () {

    }

    void Update () {
        modifyStandOnArea();
    }

    void modifyStandOnArea() {
        float posX = this.gameObject.transform.position.x;
        float posZ = this.gameObject.transform.position.z;
        if (posZ >= FenchLocation.FenchHori) {
            if (posX < FenchLocation.FenchVertLeft)
                standOnArea = 0;
            else if (posX > FenchLocation.FenchVertRight)
                standOnArea = 2;
            else
                standOnArea = 1;
        }
        else {
            if (posX < FenchLocation.FenchVertLeft)
                standOnArea = 3;
            else if (posX > FenchLocation.FenchVertRight)
                standOnArea = 5;
            else
                standOnArea = 4;
        }
    }
}

这个就是一些界面上的提示信息,预制好一个prefab,代表分数的面板和倒计时的面板和结束游戏的面板,起不同的名字,通过name区分并根据游戏进展做调整
GameStatusText.cs

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


public class GameStatusText : MonoBehaviour {
    private int score = 0;
    private int textType;
    private bool isEnd = false;
    private Timer _timer;

    void Start () {
        _timer = new Timer(30);
        _timer.tickEvent += SendMessage;
        _timer.StartTimer();
        distinguishText();
    }

    void Update () {
        _timer.UpdateTimer(Time.deltaTime);
        if (_timer.DisplayTime() <= 0)
            gameOver();
        if (textType == 2 && !isEnd){
                gameObject.GetComponent<Text>().text = "剩余时间:" + (_timer.DisplayTime() >= 0 ? _timer.DisplayTime() : 0) + " 秒";
        }


    }
    private void SendMessage()

    {

        Debug.Log("Game Over!");

    }

    void distinguishText() {
        if (gameObject.name.Contains("Score"))
            textType = 0;
        else if (gameObject.name.Contains("GameOver"))
            textType = 1;
        else
            textType = 2;
    }

    void OnEnable() {
        GameEventManager.myGameScoreAction += gameScore;
        GameEventManager.myGameOverAction += gameOver;
    }

    void OnDisable() {
        GameEventManager.myGameScoreAction -= gameScore;
        GameEventManager.myGameOverAction -= gameOver;
    }

    void gameScore() {
        if(!isEnd)
            score++;
        if (!isEnd && textType == 0) {

            this.gameObject.GetComponent<Text>().text = "得分: " + score;
        }
    } 

    void gameOver() {
        isEnd = true;
        _timer.StopTimer();
        if (textType == 1)
        {
            this.gameObject.GetComponent<Text>().text = "游戏结束!\n您的总得分为:" + score;


        }
        if(textType == 0)
        {
            this.gameObject.GetComponent<Text>().text = "游戏已结束! 将不再计分";
        }
        if(textType == 2)
        {
            this.gameObject.GetComponent<Text>().text = "游戏时间:" + _timer.GetTime() + " 秒";
        }

    }
}

这个就类似于导演的控制,大概作用就是实现巡逻兵的循环走动,并且有随机的方向(如果有玩家出现在一定范围内方向就是对着玩家的反向),还有判断是否走出界
GameModel.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;

public class GameModel : SSActionManager, ISSActionCallback {
    public GameObject PatrolItem, HeroItem, sceneModelItem, canvasItem;

    private SceneController scene;
    private GameObject myHero, sceneModel, canvasAndText;
    private List<GameObject> PatrolSet;
    private List<int> PatrolLastDir;

    private const float PERSON_SPEED_NORMAL = 0.05f;
    private const float PERSON_SPEED_CATCHING = 0.06f;

    void Awake() {
        PatrolFactory.getInstance().initItem(PatrolItem);
    }

    protected new void Start () {
        scene = SceneController.getInstance();
        scene.setGameModel(this);

        genHero();
        genPatrols();
        sceneModel = Instantiate(sceneModelItem);
        canvasAndText = Instantiate(canvasItem);
    }

    protected new void Update() {
        base.Update();
    }

    void genHero() {
        myHero = Instantiate(HeroItem);
    }

    void genPatrols() {
        PatrolSet = new List<GameObject>(6);
        PatrolLastDir = new List<int>(6);
        Vector3[] posSet = PatrolFactory.getInstance().getPosSet();
        for (int i = 0; i < 6; i++) {
            GameObject newPatrol = PatrolFactory.getInstance().getPatrol();
            newPatrol.transform.position = posSet[i];
            newPatrol.name = "Patrol" + i;
            PatrolLastDir.Add(-2);
            PatrolSet.Add(newPatrol);
            addRandomMovement(newPatrol, true);
        }
    }

    public void heroMove(int dir) {
        myHero.transform.rotation = Quaternion.Euler(new Vector3(0, dir * 90, 0));
        switch (dir) {
            case Diretion.UP:
                myHero.transform.position += new Vector3(0, 0, 0.1f);
                break;
            case Diretion.DOWN:
                myHero.transform.position += new Vector3(0, 0, -0.1f);
                break;
            case Diretion.LEFT:
                myHero.transform.position += new Vector3(-0.1f, 0, 0);
                break;
            case Diretion.RIGHT:
                myHero.transform.position += new Vector3(0.1f, 0, 0);
                break;
        }
    }
    public void SSActionEvent(SSAction source, SSActionEventType eventType = SSActionEventType.Completed, 
        SSActionTargetType intParam = SSActionTargetType.Normal, string strParam = null, object objParam = null) {
        if (intParam == SSActionTargetType.Normal)
            addRandomMovement(source.gameObject, true);
        else
            addDirectMovement(source.gameObject);
    }

    public void addRandomMovement(GameObject sourceObj, bool isActive) {
        int index = getIndexOfObj(sourceObj);
        int randomDir = getRandomDirection(index, isActive);
        PatrolLastDir[index] = randomDir;

        sourceObj.transform.rotation = Quaternion.Euler(new Vector3(0, randomDir * 90, 0));
        Vector3 target = sourceObj.transform.position;
        switch (randomDir) {
            case Diretion.UP:
                target += new Vector3(0, 0, 1);
                break;
            case Diretion.DOWN:
                target += new Vector3(0, 0, -1);
                break;
            case Diretion.LEFT:
                target += new Vector3(-1, 0, 0);
                break;
            case Diretion.RIGHT:
                target += new Vector3(1, 0, 0);
                break;
        }
        addSingleMoving(sourceObj, target, PERSON_SPEED_NORMAL, false);
    }
    int getIndexOfObj(GameObject sourceObj) {
        string name = sourceObj.name;
        char cindex = name[name.Length - 1];
        int result = cindex - '0';
        return result;
    }
    int getRandomDirection(int index, bool isActive) {
        int randomDir = Random.Range(-1, 3);
        if (!isActive) {  
            while (PatrolLastDir[index] == randomDir || PatrolOutOfArea(index, randomDir)) {
                randomDir = Random.Range(-1, 3);
            }
        }
        else {          
            while (PatrolLastDir[index] == 0 && randomDir == 2 
                || PatrolLastDir[index] == 2 && randomDir == 0
                || PatrolLastDir[index] == 1 && randomDir == -1
                || PatrolLastDir[index] == -1 && randomDir == 1
                || PatrolOutOfArea(index, randomDir)) {
                randomDir = Random.Range(-1, 3);
            }
        }
        return randomDir;
    }
    bool PatrolOutOfArea(int index, int randomDir) {
        Vector3 patrolPos = PatrolSet[index].transform.position;
        float posX = patrolPos.x;
        float posZ = patrolPos.z;
        switch (index) {
            case 0:
                if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertLeft
                    || randomDir == 2 && posZ - 1 < FenchLocation.FenchHori)
                    return true;
                break;
            case 1:
                if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertRight
                    || randomDir == -1 && posX - 1 < FenchLocation.FenchVertLeft
                    || randomDir == 2 && posZ - 1 < FenchLocation.FenchHori)
                    return true;
                break;
            case 2:
                if (randomDir == -1 && posX - 1 < FenchLocation.FenchVertRight
                    || randomDir == 2 && posZ - 1 < FenchLocation.FenchHori)
                    return true;
                break;
            case 3:
                if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertLeft
                    || randomDir == 0 && posZ + 1 > FenchLocation.FenchHori)
                    return true;
                break;
            case 4:
                if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertRight
                    || randomDir == -1 && posX - 1 < FenchLocation.FenchVertLeft
                    || randomDir == 0 && posZ + 1 > FenchLocation.FenchHori)
                    return true;
                break;
            case 5:
                if (randomDir == -1 && posX - 1 < FenchLocation.FenchVertRight
                    || randomDir == 0 && posZ + 1 > FenchLocation.FenchHori)
                    return true;
                break;
        }
        return false;
    }
    public void addDirectMovement(GameObject sourceObj) {
        int index = getIndexOfObj(sourceObj);
        PatrolLastDir[index] = -2;

        sourceObj.transform.LookAt(sourceObj.transform);
        Vector3 oriTarget = myHero.transform.position - sourceObj.transform.position;
        Vector3 target = new Vector3(oriTarget.x / 4.0f, 0, oriTarget.z / 4.0f);
        target += sourceObj.transform.position;
        addSingleMoving(sourceObj, target, PERSON_SPEED_CATCHING, true);
    }

    void addSingleMoving(GameObject sourceObj, Vector3 target, float speed, bool isCatching) {
        this.runAction(sourceObj, CCMoveToAction.CreateSSAction(target, speed, isCatching), this);
    }

    void addCombinedMoving(GameObject sourceObj, Vector3[] target, float[] speed, bool isCatching) {
        List<SSAction> acList = new List<SSAction>();
        for (int i = 0; i < target.Length; i++) {
            acList.Add(CCMoveToAction.CreateSSAction(target[i], speed[i], isCatching));
        }
        CCSequeneActions MoveSeq = CCSequeneActions.CreateSSAction(acList);
        this.runAction(sourceObj, MoveSeq, this);
    }

    public int getHeroStandOnArea() {
        return myHero.GetComponent<HeroStatus>().standOnArea;
    }
}

这个脚本主要是确定游戏状态,游戏在进行就加分数,减少时间,如果结束就弹出结束面板
GameEventManager.cd

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;

public class GameEventManager : MonoBehaviour {
    public delegate void GameScoreAction();
    public static event GameScoreAction myGameScoreAction;

    public delegate void GameOverAction();
    public static event GameOverAction myGameOverAction;

    private SceneController scene;

    void Start () {
        scene = SceneController.getInstance();
        scene.setGameEventManager(this);
    }

    void Update () {

    }

    public void heroEscapeAndScore() {
        if (myGameScoreAction != null)
            myGameScoreAction();
    }

    public void patrolHitHeroAndGameover() {
        if (myGameOverAction != null)
            myGameOverAction();
    }
}

这里加了个计算倒计时的类,默认游戏时长为30s,如果时间到了,即使没有碰到巡逻兵,也会结束游戏
Timer.cs

public class Timer

{

bool _isTicking;//是否在计时中

float _currentTime;//当前时间

float _endTime;//结束时间

public delegate void EventHander();

public event EventHander tickEvent;

public Timer(float second)

{

_currentTime = 0;

_endTime = second;

}

/// <summary>

/// 开始计时

/// </summary>

public void StartTimer()

{

_isTicking = true;

}

/// <summary>

/// 更新中

/// </summary>

public void UpdateTimer(float deltaTime)

{

if (_isTicking)

{

_currentTime += deltaTime;

if (_currentTime > _endTime)

{

_isTicking = false;

tickEvent();

}

}

}

/// <summary>

/// 停止计时

/// </summary>

public void StopTimer()

{

_isTicking = false;

}

/// <summary>

/// 持续计时

/// </summary>

public void ContinueTimer()

{

_isTicking = true;

}

/// <summary>

/// 重新计时

/// </summary>

public void ReStartTimer()

{

_isTicking = true;

_currentTime = 0;

}

/// <summary>

/// 重新设定计时器

/// </summary>

public void ResetEndTimer(float second)

{

_endTime = second;

}

public float DisplayTime()
    {
        return _endTime - _currentTime;
    }
public float GetTime()
    {
        if (_currentTime <= _endTime)
            return _currentTime;
        else
            return _endTime;
    }

}

另外就是具体的动作实现的一些类
CCMoveToAction.cs

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

public class CCMoveToAction: SSAction {
    public Vector3 target;
    public float speed;
    public bool isCatching; 

    public static CCMoveToAction CreateSSAction(Vector3 _target, float _speed, bool _isCatching) {
        CCMoveToAction action = ScriptableObject.CreateInstance<CCMoveToAction>();
        action.target = _target;
        action.speed = _speed;
        action.isCatching = _isCatching;
        return action;
    }

    public override void Start() {

    }

    public override void Update() {
        this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed);
        if (this.transform.position == target) {
            this.destroy = true;
            if (!isCatching)   
                this.callBack.SSActionEvent(this);
            else
                this.callBack.SSActionEvent(this, SSActionEventType.Completed, SSActionTargetType.Catching);
        }
    }
}

CCSequenceAction.cs

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

public class CCSequeneActions : SSAction, ISSActionCallback {
    public List<SSAction> actionList;
    public int repeatTimes = -1;  
    public int subActionIndex = 0;  

    public static CCSequeneActions CreateSSAction(List<SSAction> _actionList, int _repeatTimes = 0) {
        CCSequeneActions action = ScriptableObject.CreateInstance<CCSequeneActions>();
        action.repeatTimes = _repeatTimes;
        action.actionList = _actionList;
        return action;
    }

    public override void Start() {
        foreach (SSAction action in actionList) {
            action.gameObject = this.gameObject;
            action.transform = this.transform;
            action.callBack = this;
            action.Start();
        }
    }

    public override void Update() {
        if (actionList.Count == 0)
            return;
        else if (subActionIndex < actionList.Count) {
            actionList[subActionIndex].Update();
        }
    }
    public void SSActionEvent(SSAction source, 
        SSActionEventType eventType = SSActionEventType.Completed,
        SSActionTargetType intParam = SSActionTargetType.Normal, string strParam = null, object objParam = null) {

        source.destroy = false;
        this.subActionIndex++;
        if (this.subActionIndex >= actionList.Count) {
            this.subActionIndex = 0;
            if (repeatTimes > 0)
                repeatTimes--;
            if (repeatTimes == 0) {
                this.destroy = true;
                this.callBack.SSActionEvent(this);
            }
        }
    }

    void OnDestroy() {

    }
}

ISSActionCallback.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

public enum SSActionEventType: int { Started, Completed }
public enum SSActionTargetType : int { Normal, Catching } 

public interface ISSActionCallback {
    void SSActionEvent(SSAction source,
        SSActionEventType eventType = SSActionEventType.Completed,
        SSActionTargetType intParam = SSActionTargetType.Normal,     
        string strParam = null,
        Object objParam = null);
}

SSAction.cs

using UnityEngine;
using System.Collections;

public class SSAction : ScriptableObject {
    public bool enable = true;
    public bool destroy = false;

    public GameObject gameObject { get; set; }
    public Transform transform { get; set; }
    public ISSActionCallback callBack { get; set; }

    protected SSAction() { }

    public virtual void Start() {
        throw new System.NotImplementedException();
    }

    public virtual void Update() {
        throw new System.NotImplementedException();
    }
}

SSActionManager.cs

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

public class SSActionManager : MonoBehaviour {

    private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
    private List<SSAction> waitingAdd = new List<SSAction>();
    private List<int> waitingDelete = new List<int>();

    protected void Start () {

    }

    protected void Update() {
        foreach (SSAction ac in waitingAdd) {
            actions[ac.GetInstanceID()] = ac;
        }
        waitingAdd.Clear();

        foreach (KeyValuePair<int, SSAction> kv in actions) {
            SSAction ac = kv.Value;
            if (ac.destroy)
                waitingDelete.Add(kv.Key);
            else if (ac.enable)
                ac.Update();
        }

        foreach (int key in waitingDelete) {
            SSAction ac = actions[key];
            actions.Remove(key);
            DestroyObject(ac);
        }
        waitingDelete.Clear();
    }

    public void runAction(GameObject gameObj, SSAction action, ISSActionCallback manager) {
        for (int i = 0; i < waitingAdd.Count; i++) {
            if (waitingAdd[i].gameObject.Equals(gameObj)) {
                SSAction ac = waitingAdd[i];
                waitingAdd.RemoveAt(i);
                i--;
                DestroyObject(ac);
            }
        }
        foreach (KeyValuePair<int, SSAction> kv in actions) {
            SSAction ac = kv.Value;
            if (ac.gameObject.Equals(gameObj)) {
                ac.destroy = true;
            }
        }

        action.gameObject = gameObj;
        action.transform = gameObj.transform;
        action.callBack = manager;
        waitingAdd.Add(action);
        action.Start();
    }
}

猜你喜欢

转载自blog.csdn.net/jankingmeaning/article/details/80285585