Shaderというやつを使って何かちょっとした被弾エフェクトを作ろうとした
ちょっとした企画で勉強として簡単なゲームを作っていたのですが、弾が敵にあたって弾が消えるだけでは当てた感じがなく寂しい仕上がりになりました。そこでシェーダーをつかってちょっとしたエフェクトを作ろうということに。そこで死ぬほど苦労したのでメモ書き程度に書いておこうかなと思いました。
まず最初にやるべきこと
Main CameraにLightコンポーネントをつけてTypeをDirectionalにしましょう。こうしないと設定した色より暗い色になってしまったりします。
問題1.まずどんな感じに設定すればシェーダーを適応させられるのか
なにもないところにシェーダ-は発動させられません。まずは画像を用意しましょう。白の正方形でいいです。僕は名前を"white"にしました。この画像をシーンに持ってきてオブジェクトとしておいてみましょう。これが開発画面では見えるけど、ゲーム内で見えない場合z座標を調整することで見えるようになります。
このオブジェクトのSprite RenderereのところにはMaterialという欄があるのでここに自分で作ったマテリアルを入れましょう。僕は"HitEffectMaterial"という名前のマテリアルを作っていれました。
これでもまだ終わりではありません。当然シェーダ-が必要です。自分でシェーダーのファイルを作りこれをマテリアルに入れます。僕は"HitEffectShader"という名前にしました。中身はこんな感じです。
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/HitEffectShader"
{
// BG_shader.shader の Properties を追加
Properties
{
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Standard alpha:fade
#pragma target 3.0
struct Input {
float3 worldPos;
};
uniform float _StartTime;
void surf (Input IN, inout SurfaceOutputStandard o) {
float3 localPos = IN.worldPos - mul(unity_ObjectToWorld, float4(0, 0, 0, 1)).xyz;
float dist = distance(fixed3(0, 0, 0), localPos);
float radius = (_Time.y )*5;
float radius2 = (_Time.y )*4.8+0.2;
if( radius < dist &&radius2>dist){
o.Albedo = fixed4(220/255.0, 20/255.0, 60/255.0, 1);
o.Alpha = 1.0f;
} else {
o.Albedo = fixed4(1,1,1,1);
o.Alpha = 0.0f;
}
}
ENDCG
}
FallBack "Diffuse"
}
Propertiesの中に _MainTex("2D Texture", 2D) = "white" {} と書くとなおります。
このコードに関しては
2D Spriteにシェーダーをかける
円やリングをかっこよく動かす方法
を参考にさせていただきました。
問題2.何か一個のエフェクトを出すと他のエフェクトが消えてしまう
マテリアルが共通しているオブジェクトを複数複製して、そのうち1つのマテリアルを編集すると全てのオブジェクトに適応されてしまうという話を聞いたのでそれ関連のやつかなと思いました。
そこで自分が発射した弾に加えたスクリプトです。基本的に殆ど読み飛ばしてもらって構いません。注目してほしいのが41~48行の範囲です。マテリアルを新しいマテリアルで書き換えて、エフェクト発生場所を弾丸の場所にする。これで解決します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BulletController : MonoBehaviour
{
[SerializeField]
private GameObject GameDataMemo;
[SerializeField]
private GameObject HitEffect;
[SerializeField]
private float BulletSpeed;
[SerializeField]
private float BulletDamage;
// Start is called before the first frame update
public void BulletSpeedSetter(float tmp)
{
BulletSpeed = tmp;
}
public void BulletSpeedChanger(float tmp)
{
BulletSpeed += tmp;
}
public void BulletDamageSetter(float tmp)
{
BulletDamage = tmp;
}
public void BulletDamageChanger(float tmp)
{
BulletDamage += tmp;
}
private void OnTriggerEnter2D(Collider2D collider)
{
if (collider.gameObject.tag != "Enemy")
{
return;
}
Debug.Log(HitEffect);
collider.GetComponent<EnemyController>().HPChanger(-BulletDamage);
GameObject eff;
eff=Instantiate(HitEffect);
Material mat = eff.GetComponent<Renderer>().material;
eff.GetComponent<Renderer>().material = new Material(mat);
Vector3 pos = GetComponent<Transform>().position;
eff.GetComponent<Transform>().position = pos;
Debug.Log(Time.time);
Debug.Log(eff.GetComponent<Renderer>().material);
Destroy(gameObject);
}
void Start()
{
GameDataMemo = GameObject.Find("GameDataMemo");
//HitEffect = GameObject.Find("HitEffectPrefab");
}
// Update is called once per frame
void Update()
{
Vector3 pos = GetComponent<Transform>().position;
pos.x += BulletSpeed * Time.deltaTime;
transform.position = pos;
GameDataMemo Limit = GameDataMemo.GetComponent<GameDataMemo>();
if(pos.x>Limit.RightLimitGetter()|| pos.x < Limit.LeftLimitGetter() || pos.y > Limit.UpLimitGetter() || pos.y < Limit.DownLimitGetter())
{
Destroy(gameObject);
}
}
}
問題3.シェーダー内で使える時間はシーン開始からの経過時間でオブジェクト生成からの時間ではない
デフォルトで_Timeというものがありこれが時間を表しているがオブジェクト生成時の時間を持ってこれないからどうしようもないと悩んでいました。しかしshader内のProperties内に変数を作りそこにC#のスクリプトから代入できることがわかりました。今回は時間を保有したいのでFloat型でいきます(floatじゃなくFloatらしいです。)。
文法は変数名("インスペクタで見える名前",型)=初期値で、改めてSubShader内で宣言(このときはFloatではなくfloatで、書かなくてもいいけどuniformをその前に書くみたい)。
ここらへんの詳しい話は最後の参考資料に載せておきます。
最終的にシェーダーは以下のようになりました。
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/HitEffectShader"
{
// BG_shader.shader の Properties を追加
Properties
{
_MainTex("2D Texture", 2D) = "white" {}
_StartTime("StartTime",Float) = 0.0//ここに注目
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Standard alpha:fade
#pragma target 3.0
struct Input {
float3 worldPos;
};
uniform float _StartTime;//ここに注目
void surf (Input IN, inout SurfaceOutputStandard o) {
float3 localPos = IN.worldPos - mul(unity_ObjectToWorld, float4(0, 0, 0, 1)).xyz;
float dist = distance(fixed3(0, 0, 0), localPos);
float radius = (_Time.y - _StartTime)*5;
float radius2 = (_Time.y - _StartTime)*4.8+0.2;
if( radius < dist &&radius2>dist){
o.Albedo = fixed4(220/255.0, 20/255.0, 60/255.0, 1);
o.Alpha = 1.0f;
} else {
o.Albedo = fixed4(1,1,1,1);
o.Alpha = 0.0f;
}
}
ENDCG
}
FallBack "Diffuse"
}
最終的に弾丸のスクリプトは以下のようになりました。50行目に追加されているものが本質です。これによって現在の時間(オブジェクトが生成された時間)がシェーダーの_StartTimeに代入されます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BulletController : MonoBehaviour
{
[SerializeField]
private GameObject GameDataMemo;
[SerializeField]
private GameObject HitEffect;
[SerializeField]
private float BulletSpeed;
[SerializeField]
private float BulletDamage;
// Start is called before the first frame update
public void BulletSpeedSetter(float tmp)
{
BulletSpeed = tmp;
}
public void BulletSpeedChanger(float tmp)
{
BulletSpeed += tmp;
}
public void BulletDamageSetter(float tmp)
{
BulletDamage = tmp;
}
public void BulletDamageChanger(float tmp)
{
BulletDamage += tmp;
}
private void OnTriggerEnter2D(Collider2D collider)
{
if (collider.gameObject.tag != "Enemy")
{
return;
}
Debug.Log(HitEffect);
collider.GetComponent<EnemyController>().HPChanger(-BulletDamage);
GameObject eff;
eff=Instantiate(HitEffect);
Material mat = eff.GetComponent<Renderer>().material;
eff.GetComponent<Renderer>().material = new Material(mat);
Vector3 pos = GetComponent<Transform>().position;
eff.GetComponent<Transform>().position = pos;
eff.GetComponent<Renderer>().material.SetFloat("_StartTime", Time.time);//ここに注目
Debug.Log(Time.time);
Debug.Log(eff.GetComponent<Renderer>().material);
Destroy(gameObject);
}
void Start()
{
GameDataMemo = GameObject.Find("GameDataMemo");
//HitEffect = GameObject.Find("HitEffectPrefab");
}
// Update is called once per frame
void Update()
{
Vector3 pos = GetComponent<Transform>().position;
pos.x += BulletSpeed * Time.deltaTime;
transform.position = pos;
GameDataMemo Limit = GameDataMemo.GetComponent<GameDataMemo>();
if(pos.x>Limit.RightLimitGetter()|| pos.x < Limit.LeftLimitGetter() || pos.y > Limit.UpLimitGetter() || pos.y < Limit.DownLimitGetter())
{
Destroy(gameObject);
}
}
}
問題4.Order in Layerの値をあげてもエフェクトが他オブジェクトの下に来る
MaterialのRender Queueの値をあげましょう。僕はそれで直った。
まとめ
- DirectionalなLightをカメラにつけよう。
- シェーダーはマテリアルに、マテリアルはオブジェクトのSprite Rendererへ。
- マテリアルは複製した際に新しいマテリアルを代入することでマテリアルの変更が連動してしまうことを防げる。
- シェーダーに他のスクリプトから変数を持ち込みたいならshaderのPropertiesで宣言してスクリプトでその変数に代入しましょう。
- 前面に出したいならRender Queueの値をあげましょう。
参考にした記事など
2D Spriteにシェーダーをかける
円やリングをかっこよく動かす方法
その2 ShaderLabでUnityシェーダの下地作り
スクリプトからShaderパラメータを変更する(例:_EmissionColorについて)