Unity2D游戏


一、图片

1 人物

Pixels Per Unit:每个单元的像素。值越大图显示越小。

Pivot:中心点。

Default——Max Size:一般选宽高中的大的值的接近值。例如290 * 500 选 512。

2 问题

  1. 图片太小:调节图片的Pixels Per Unit。

二、组件

1 Sprite Renderer

Sorting Layer:排序层。可以添加层,越在前面的层显示越在屏幕的前面。一般用Default设置Order in Layer既可。

Order in Layer:层。值越大显示越在屏幕前。

2 Camera

Projection:Orthographic。

size:相机中心到边缘的unit个数。


三、动画

1 人物动画

新建动画后,直接旋中所有图片拖拽激到animation中,再点击右边三个小点的Show Sample Rate(每秒帧数)进行调整既可。

2 Chinemachine

安装:

  1. Window——Package Manager——Packages:Unity Registry——Chinemachine——install
  2. Hierarchy右键——Chinemachine——2D Camera。(此时主摄像机由CM vcam1控制)

CM vcam1:

  • Follow:跟随对象。
  • Lens——Ortho Size:网格大小。
  • Body:
    • Damping:当人物离中心点 n 距离摄像机才开始移动。
    • Screen:人物所处位置。
    • Dead Zone:在范围中相机完全不移动。
    • Soft Zone:蓝色区域大小。玩家可移动的区域。红色区域玩家不可到达。
    • Bias:蓝色区域移动。

边界:

  1. Hierarchy右键——Create——Create Empty
  2. Add Component——Polygon Collider 2D
  3. Points——Path——Element0——Size:4
  4. 拖动边界为整个地图。
  5. CM vcam1——Extensions——Add Extension——Cinemachine Confiner
  6. 拖入空物体至Bounding Shape 2D

四、脚本

1 属性

// 自身个体属性
public float speed;
public int enemyLife;

// 自身其他属性
public Vector3 targetPosition, originPosition, terminalPosition;

// 自身状态
private bool isAfterBattleCheck;

// 自身组件
private BoxCollider2D box;
private Sprite
private Animator animator;
public GameObject attackCollider;

// 外部组件
private GameObject player;

2 移动

// 上下左右移动
void FixedUpdate()
{
Move();
}

private void Move()
{
float leftRight = Input.GetAxisRaw("Horizontal");
float upDown = Input.GetAxisRaw("Vertical");
// 转头
if (leftRight > 0)
{
transform.localScale = new Vector3(1f, 1f, 1f);
}
else if (leftRight < 0)
{
transform.localScale = new Vector3(-1f, 1f, 1f);
}
// 移动
transform.Translate(leftRight * Time.fixedDeltaTime * speed, upDown * Time.fixedDeltaTime * speed, 0);
// 动画
animator.SetFloat("Run", Mathf.Max(Mathf.Abs(leftRight), Mathf.Abs(upDown)));
}

// 左右加跳跃
public class PlayerControl : MonoBehaviour
{
private Rigidbody2D rb;

[SerializeField] private float moveSpeed;
[SerializeField] private float jumpForce;
private float xInput;

void Start()
{
rb = GetComponent<Rigidbody2D>();
}

void Update()
{
Movement();
CheckInput();
}

private void CheckInput()
{
xInput = Input.GetAxisRaw("Horizontal");

if (Input.GetKeyDown(KeyCode.Space))
{
Jump();
}
}

private void Movement()
{
rb.velocity = new Vector2(xInput * moveSpeed, rb.velocity.y);
}

private void Jump()
{
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
}
}

3 设置人物属性

public class ShowInInspector : MonoBehaviour
{
[System.Serializable]
public struct Attributes
{
public int HP;
public int MP;
}

[System.Serializable]
public struct Character
{
public int skinID;
public int characterID;
}

// 是否在属性前加上文字,后面必须跟公开属性或序列化的私有属性
// [Header("Attributes")]
// 属性中有子属性
public Attributes attribute;

//[Header("Character")]
public Character character;

// 在检视器中会新增一个空格
[Space]
}

五、场景

1 Tile Palette

创建 Palette:

  1. Window——2D——Tile Palette——Create New Palette。

  2. 拖入图片,图片转换格式后存入一个新文件夹中。

Edit Palette:

  • S:选中物体
  • M:移动物体
  • B:选取后直接移动到场景中。
  • I:先选中,后按左键复制物体。
  • D:擦去场景中的错误。

使用 Palette:

  1. Hierarchy右键——Create——2D Object——Tilemap
  2. 在Palette中选择Active Tilemap
  3. 在Palette中安B选择物体移动到场景中。

碰撞器:

  1. 添加Tilemap Collider 2D。(该项比较消耗资源,需要进行后续设置)
  2. 添加Composite Collider 2D,会自动添加Rigidbody 2D。
  3. Rigidbody 2D——Body Type——Static。
  4. Tilemap Collider 2D——Used By Composite。

2 边界

限定边界用碰撞器。当玩家碰到底部物件(通过底部物件的名字判断)时直接杀死玩家。

3 平台

可穿越平台:

  1. 设置一个父空物件,将地形作为该父物件的子物件。
  2. 给父物件添加Platform Effector 2D和Box Collider,Box Collider选择Used By Effector
  3. 设置Box Collider 2D的大小,使其大小仅仅为地面区域。

Platform Effector 2D——Surface Arc:作用弧度。

// 可移动平台
public class PlatformMove : MonoBehaviour
{
public Vector3 originPosition, targetPosition, terminalPosition;
public float moveSpeed;

private void Start()
{
targetPosition = terminalPosition;
originPosition = transform.position;
}

// Update is called once per frame
private void Update()
{
Move();
}

private void Move()
{
if (transform.position == targetPosition)
{
if (targetPosition == originPosition)
{
targetPosition = terminalPosition;
}
else
{
targetPosition = originPosition;
}
}
transform.position = Vector3.MoveTowards(transform.position, targetPosition, moveSpeed * Time.deltaTime);
}
}


// 站上平台后玩家跟随移动(此处为玩家脚本)
private void OnTriggerEnter2D(Collider2D collision)
{
if(collision.tag == "AirPlatform")
{
/*此处还需补充玩家的Jump相关方法*/
playerScript.transform.parent = collision.transform;
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if(collision.tag == "AirPlatform")
{
playerScript.transform.parent = null;
}
}

4 UI

  1. Canvas——Render Mode——Screen Space-Camera
  2. 拖入主摄像机
  3. Order in Layer设置大于场景Order in Layer

Canvas Renderer 的 “Cull Transparent Mesh” 属性可以根据 UI 元素的透明度进行渲染剔除,以提高渲染性能。需要注意的是,这个属性只会影响透明的部分,对不透明的像素没有影响。


六、设计

1 路线设计

根路线——多条子路线——多个路线点。

选择时,从跟路线出发,选择多条子路线中的一条以此保证随机性。

这些对象都是空物体。


七、打包

手机型号查询:

  1. Package Manager——Device Simulator

  2. Simulator——将game改成Simulator

  3. 查看手机的Safe Area

背景有留白:

多创建两张背景,将背景进行拼接,从而解决留白。

// 创建一个SafePanel
// 拖入代码自动调节SafeArea区域大小
// 其他控件作为SafePanel的子物件
public class SafeAreaPanel : MonoBehaviour
{
private RectTransform panel;

void Start()
{
panel = GetComponent<RectTransform>();

Vector2 safeAreaMinPosition = Screen.safeArea.position;
Vector2 safeAreaMaxPosition = Screen.safeArea.position + Screen.safeArea.size;

safeAreaMinPosition.x = safeAreaMinPosition.x / Screen.width;
safeAreaMinPosition.y = safeAreaMinPosition.y / Screen.height;

safeAreaMaxPosition.x = safeAreaMaxPosition.x / Screen.width;
safeAreaMaxPosition.y = safeAreaMaxPosition.y / Screen.height;

panel.anchorMin = safeAreaMinPosition;
panel.anchorMax = safeAreaMaxPosition;
}
}

八、问题

1 物件翻转

刚体中设置固定Z轴。

2 墙壁

卡墙:给玩家的Rigidbody 2D加Material。Material的Friction设置为0。

靠墙跳无法触发跳跃动画:父物体下加一个空物体,空物体加上Box Collider 2D,选上Is Trigger,拉到人物底部,调整大小为宽度同人物宽,高度尽可能小。

// 靠墙跳无法触发跳跃动画,脚本挂在空物体上
public class PlayerTrigger : MonoBehaviour
{
private PlayerControl playerScript;
void Start()
{
// 获得父物体脚本,用于下面设置参数
playerScript = GetComponentInParent<PlayerControl>();
}

private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == "Ground")
{
playerScript.canJump = true;
playerScript.animator.SetBool("Jump", false);
}
}
}

3 碰撞器

碰撞器位置:

  • 调整好碰撞器后,当动画发生切换时,此时图片大小会发生变化,碰撞器位置也会发生变化。

  • 解决方法:可以点击图片——Privot——Custom手动调整偏移。

碰撞器休眠:

  • 当玩家不移动时,碰撞器会休眠。此时再攻击会无法触发攻击效果。

  • 解决方法1:Rigidbody 2D——Sleeping Mode——Never Sleep(注:该设置十分消耗性能,一般只给玩家设置即可)

  • 解决方法2:玩家的判断攻击的子物体中加上Rigidbody 2D——Body Type——Kinematic。此时玩家不需要再设置为Never Sleep了。因为Collider中的Info的判定信息变成了攻击的子物体而不是玩家。

无法重复碰撞:

  • 当玩家受伤后,敌人进入了玩家体内,此时当无敌结束后,玩家不会再收到伤害。原因是因为Trigger只有Enter没有Stay

  • 解决方法:让玩家既有 OnTriggerEnter又有 OnTriggerStay。其内部代码是一样的。

摄像头碰撞器:

  • 当给摄像头添加了碰撞器后,人物在碰到摄像头后会出现在异常位置。

  • 解决方法:新加一个Layer为BoundCamera。然后在Physics 2D中取消BoundCamera与别的层的碰撞,只保留和自己的。

触发器重叠:

  • 玩家下的子物体和玩家都有触发器时,玩家的子物体触发时会判定被触发对象为玩家。

  • 解决方法:给子物体加一个Rigidbody 2D,设置Body Type为Kinematic。

4 重复动画播放

设置了trigger的属性可能会重复播放动画。

解决方法:需要在动画的最后一帧加上事件,事件绑定函数重置触发器。

// 动画
private void Update()
{
float swordAttack = Input.GetAxisRaw("Sword");
if (swordAttack > 0)
{
animator.SetTrigger("Attack");
isAttack = true;
canJump = false;
}
}
// 重置触发器
public void SetIsAttackFalse()
{
isAttack = false;
canJump = true;
animator.ResetTrigger("Attack");
}

5 受伤

在播放受伤动画时输入的值会被记录,导致动画结束后会立刻执行响应动作。(例如:被击飞时按了闪现,落地后会立刻闪现)

受伤后不会无敌,会重复受伤。

解决方法:在相应条件后面加上&& isHurt == false。例如:if (jump > 0 && canJump == true && isHurt == false)

6 远程武器被挡

武器设定碰撞为if (collision != null && collision.tag != "Player")。当玩家使用远程武器时,武器会被敌人设定的AttackCollider空物体给摧毁。

解决方法:设定Layer,玩家攻击为PlayerAttack,敌人攻击为EnemyAttack。然后到Edit——Project Setting——Physics 2D,然后把两者的作用关系的勾取消掉即可。

7 背景

背景不随人物移动。

解决方法:拖动背景作为主摄像机的子物体。

8 寻路穿墙

某些敌方单位会跟随玩家移动,此时会发生穿墙,浮空等BUG。

解决方法:

  1. 在地方单位两侧设置停止点,停止点为空物体,加上触发器。
  2. 给地方单位加上刚体,设置为Body Type——Kinematic。

9 寻找物件

// 找到没有父物件的Canvas
canvas = GameObject.Find("/Canvas").GetComponent<Canvas>();

10 子弹判定

因为速度太快,用碰撞和触发不一定会触发,所以要用射线判定。

图层:设置哪一层射线就检测哪一层。可以给敌人专门做一个层。

using UnityEngine;

public class Bullet : MonoBehaviour
{
public float speed = 50f;
public float maxDistance = 300f;
public LayerMask hitLayers; // 可以被射线击中的层级

private void Update()
{
// 根据速度求得子弹的移动距离
float distance = speed * Time.deltaTime;

RaycastHit hit;
// 起点,终点,击中信息,最远距离,图层
if (Physics.Raycast(transform.position, transform.forward, out hit, distance, hitLayers))
{
// 如果射线击中任何物体,通过碰撞信息做相应处理
Debug.Log("射中了:" + hit.collider.gameObject.name);

// 在此处添加根据碰撞物体进行的操作,比如造成伤害、触发特效等
}

// 更新子弹位置
transform.Translate(Vector3.forward * distance);

// 判断是否超出最大距离,如果超出则销毁子弹对象
if (Vector3.Distance(transform.parent.position, transform.position) >= maxDistance)
{
Destroy(gameObject);
}
}
}


九、优化

1 资源重复加载

为节约性能,把需要重复加载的内容放在ResourcesManager类中。然后在该类中写静态构造函数和静态方法。静态构造函数负责读取资源到类中。静态函数负责被其他类调用返回指定资源。

// 案例
public class ResourcesManager : MonoBehaviour
{
private static Dictionary<int, Sprite> spriteDict;
// 构造函数,从文件中读取资源到内存
static ResourcesManager()
{
spriteDict = new Dictionary<int, Sprite>();
SpriteAtlas sprites = Resources.Load<SpriteAtlas>("People");
foreach (Sprite sprite in sprites.GetPackables())
{
int id = int.Parse(sprite.name);
spriteDict.Add(id, sprite);
}
}
/// <summary>
/// 读取数字精灵
/// </summary>
/// <param name="number">数字</param>
/// <returns>精灵</returns>
public static Sprite LoadSprite(int number)
{
return spriteDict[number];
}
}