Cheese Trap

2019 年 1 月—2019 年 2 月|Game Jam 项目


项目信息

  • 团队规模:7 人
  • 类型:2D 聚会游戏
  • 平台:Windows
  • 引擎:Unity 5
  • 开发时间:1 周
  • itch.io: 下载
  • Github: 源代码

关于

Cheese Trap 是一款本地双人对战游戏,两名玩家扮演老鼠展开竞争。玩家可以啃食地面制造洞穴;当对手掉进洞里时,你就获胜。

时间结束时,吞食地面最多的玩家获胜。


我的贡献

  • 与另一名程序员共同实现了大部分玩法功能
  • 开发了地图纹理与洞穴纹理的制作流程
  • 实现了多种 Shader,在原始地图上呈现挖洞效果
  • 为 UI 系统实现了多种动画

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

public class HoleManager : MonoBehaviour
{
    const int texWidth = 960;
    const int texHeight = 540;
    public List holes;
    public int[] areas;

    public GameObject holeColliderPrefab;
    // playerNum should be fetched from GameManager later
    private int playerNum = 2;
    // holeTexture has the size 960x540
    // holeTexture is used to calculate holes and masks
    private Texture2D holeTexture;
    private Texture2D terrainTexture;

    public int gameLevel;

    private int[,] caramelCoolDown = new int[960,540];
    private Texture2D originalHoleTexture;

    // only for debug
    // private GameObject testTextureDisplay;

    // SpriteRenderer sr;

    // Start is called before the first frame update
    public void InitializeLevel(int level)
    {
        holes = new List();
        caramelCoolDown = new int[960, 540];
        areas = new int[playerNum + 1];
        LoadLevelTerrainTexture(level);
        InitializeHoleTexture();
    }

    // Update is called once per frame
    void Update()
    {

    }

    // judge current status of the player on the terrain
    // -1 -> die
    // 0 -> normal
    // 1 -> ice / cream e.t.c
    public int getTerrainStatus(Vector2 position){
        position.x *= 50;
        position.y *= 50;
        if ((holeTexture.GetPixel((int)position.x, (int)position.y).r != 0 && holeTexture.GetPixel((int)position.x, (int)position.y).r != 1) ||
            holeTexture.GetPixel((int)position.x, (int)position.y).g == 1)
            return -1;
        return (int)Mathf.Round(holeTexture.GetPixel((int)position.x, (int)position.y).g * 255);
    }

    public Texture2D GetHoleTexture(){
        return holeTexture;
    }

    private void InitializeHoleTexture(){
        holeTexture = new Texture2D(texWidth, texHeight);
        Color[] colors = terrainTexture.GetPixels();
        // for (int i=0; i();
        // Sprite pic = Sprite.Create(holeTexture, new Rect(0, 0, texWidth, texHeight), new Vector2(0.5f,0.5f));
        // sr.sprite = pic;

    }

    void InitializeTerrainTexture(){
        terrainTexture= new Texture2D(texWidth, texHeight);
        Color[] colors = new Color[texWidth*texHeight];
        for (int i=0; i texWidth)
            Right = texWidth - 1;
        if (Bottom < 0)
            Bottom = 0;
        if (Top > texHeight)
            Top = texHeight - 1;

        int colorIndex = 0;
        for (int x = Left; x <= Right; x++){
            for (int y = Bottom; y <= Top; y++){
                if ((x - position.x)*(x - position.x) + (y - position.y)*(y - position.y) <= radius*radius && holeTexture.GetPixel(x, y).r == 0
                   && holeTexture.GetPixel(x, y).r != 1){
                    holeTexture.SetPixel(x, y, new Color32((byte)playerID, 0, 0, 255));
                    areas[playerID]++;
                }

                colorIndex++;
            }
        }
        holeTexture.Apply();
        DisplayHoleTexture();
    }

    public void GenerateCaramelAtPoint(Vector2 position, int radius){
        int cx = (int) position.x;
        int cy = (int) position.y;
        for (int x = cx - radius; x <= cx + radius; x++){
            for (int y = cy - radius; y <= cy + radius; y++){
                if (x >= 0 && x < texWidth && y >= 0 && y < texHeight && (x-cx)*(x-cx) + (y-cy)*(y-cy) <= radius * radius &&
                    holeTexture.GetPixel(x,y).r == 0 && holeTexture.GetPixel(x,y).g != 1){
                        StartCoroutine(SetCaramel(x, y));
                    }
            }
        }
        holeTexture.Apply();
    }

    IEnumerator SetCaramel(int x, int y){
        caramelCoolDown[x,y] += 1;
        Color c = holeTexture.GetPixel(x, y);
        Color originalColor = c;
        holeTexture.SetPixel(x, y, new Color(c.r, 3.0f/255, c.b, c.a));
        yield return new WaitForSeconds(1.5f);
        if (caramelCoolDown[x,y] == 1)
            holeTexture.SetPixel(x, y, originalHoleTexture.GetPixel(x, y));
        caramelCoolDown[x,y] -= 1;
        yield return null;
    }


    public void DisplayHoleTexture(){
        GameObject map = GameObject.Find("ForeGround");
        map.GetComponent().material.SetTexture("_Mask", holeTexture);
        GameObject shadow = GameObject.Find("HoleShadow");
        shadow.GetComponent().material.SetTexture("_Mask", holeTexture);
    }
}
            

HoleManager

洞穴管理器 用于在玩家挖洞、改变地图定义时实时创建并更新地图。地图尺寸为 960×540,该脚本能够以 60 FPS 运行。


FlowAndSpinEffect

Flow and Spin Effect 是一种 UI 特效 Shader,用于在 UI 圆盘上呈现潮汐流动效果。

Shader "Custom/FlowAndSpinEffect"
{
    Properties
    {
        _MainTex ("Sprite Texture", 2D) = "white" {}
        _FlowTex ("Base (RGB)", 2D) = "white" {}
        _ScrollXSpeed("XSpeed", Range(-10, 10)) = -3
        _RotateSpeed("RotateSpeed", Range(-10, 10)) = 0.5
        _FadeAlpha("Alpha", Range(0, 1)) = 1
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always
        Blend SrcAlpha OneMinusSrcAlpha
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            sampler2D _MainTex;
            sampler2D _FlowTex;
            fixed _ScrollXSpeed;
            fixed _RotateSpeed;
            fixed _FadeAlpha;

            fixed4 frag (v2f i) : SV_Target
            {
                fixed xScrollValue = _ScrollXSpeed * _Time.y;
                fixed theta = _RotateSpeed * _Time.y;

                fixed2 pos = i.uv-fixed2(0.5, 0.5);
                fixed4 col = tex2D(_MainTex, fixed2(pos.x*cos(theta) - pos.y*sin(theta), pos.x*sin(theta) + pos.y*cos(theta)) + fixed2(0.5, 0.5));
                fixed4 flowcol = tex2D(_FlowTex, i.uv + fixed2(xScrollValue, 0));
                if ((i.uv.x-0.5) * (i.uv.x-0.5) + (i.uv.y-0.5) * (i.uv.y-0.5) > 0.25)
                    col.a = 0;
                if (col.r == 0 && col.g == 0 && col.b == 0)
                    col.a = 0;
                col.rgb = min(col.rgb + flowcol.rgb / 4, 1);
                if (col.a == 1)
                    col.a = _FadeAlpha;

                return col;
            }
            ENDCG
        }
    }
}
            

数据驱动地图

  • 为了正确显示地图,关卡画面由四个图层构成。
  • 背景层:提供静态背景信息。
  • 前景层:提供原始前景信息,同时作为洞穴 Shader 的基础纹理。
  • 阴影图:为洞穴 Shader 提供挖掘阴影所需的信息。
  • 地形图:向游戏管理器提供玩家可通行区域与障碍物位置,也用于冰面地图以营造地面湿滑的感觉。

项目复盘


做得好的地方

  • 与优秀的队友在一周内完成了一款出色的 Game Jam 游戏
  • 学会了如何编写 Shader
  • 实现了数据驱动的地图结构

需要改进的地方

  • 团队中有一名程序员经验较少,在快速开发过程中一度跟不上进度

我的收获

  • 与能力出色的开发者一起协作非常有趣
  • 学会合理管理时间;有些日子我们曾通宵赶工以确保功能正常运行