我希望在游戏中能够有昼夜变化,四季变化,这样就意味着游戏中将会有一套自己的时间算法。假设游戏中时间流逝的速度为现实世界的30倍,那么一天就等于在游戏中过了一个月。
随着游戏中时间的变化,会有日出和日落,也会有四季变化。不同的时刻,太阳光照的角度不同,人在地上的影子方向也不一样。比如夏天太阳高度会高一些,冬天则会低一些。
[img]http://dl2.iteye.com/upload/attachment/0110/4137/e5efa98d-e992-32cc-9a66-ef76ac3b0c53.gif[/img]
我把游戏世界的位置定位在地球北纬30°(跟武汉一样),那么一年中,春分和秋分两个节气的中午12时,太阳日照高度就是60°。夏至和冬至的太阳高度则要分别加减黄赤夹角(23°26′),为83.5°和36.5°。
我假设游戏中地球公转轨迹是一个非常完美的圆形,那么太阳的高度变化正好是一个正弦函数。地球公转第day天,那么转过的角度就是:
[quote]
beta = 2 * 3.1415926 * day / 360,
[/quote]
日照高度theta为:
[quote]
theta = 60°+ 23.5°* sin(beta)。
[/quote]
废话不多说了,上代码。
然后我们写一个实例代码,来看看日出日落的效果。
注意:下例中所有模型,都来源于JME3自带的例子。
随着游戏中时间的变化,会有日出和日落,也会有四季变化。不同的时刻,太阳光照的角度不同,人在地上的影子方向也不一样。比如夏天太阳高度会高一些,冬天则会低一些。
[img]http://dl2.iteye.com/upload/attachment/0110/4137/e5efa98d-e992-32cc-9a66-ef76ac3b0c53.gif[/img]
我把游戏世界的位置定位在地球北纬30°(跟武汉一样),那么一年中,春分和秋分两个节气的中午12时,太阳日照高度就是60°。夏至和冬至的太阳高度则要分别加减黄赤夹角(23°26′),为83.5°和36.5°。
我假设游戏中地球公转轨迹是一个非常完美的圆形,那么太阳的高度变化正好是一个正弦函数。地球公转第day天,那么转过的角度就是:
[quote]
beta = 2 * 3.1415926 * day / 360,
[/quote]
日照高度theta为:
[quote]
theta = 60°+ 23.5°* sin(beta)。
[/quote]
废话不多说了,上代码。
package org.pstale.utils;
import com.jme3.math.Vector3f;
/**
* 游戏中的时间系统
* @author yanmaoyuan
*
*/
public class GameDate {
// 我们假设游戏中时间流动的速度是现实的30倍!
public final static int GAME_MINUTE = 2;// 游戏1分钟 = 现实2秒
public final static int GAME_HOUR = 120;// 游戏1小时 = 60游戏分钟 = 现实2分钟
public final static int GAME_DAY = 2880;// 游戏1天 = 24游戏小时 = 现实48分钟
public final static int GAME_MONTH = 86400;// 游戏1月 =30游戏天 = 现实1天
public final static int GAME_YEAR = 1036800;// 游戏1年 = 12游戏月 = 现实12天
// 地球黄赤交角为23°26′
private final static double ECLIPTIC_OBLIQUITY = Math.PI * 23.5f / 180f;
// 假设世界的纬度为30°N,春分时中午12点太阳高度为60°
private final static double WUHAN_LATITUDE = Math.PI * 60f / 180f;
private final static double SEC_DEG = 2 * Math.PI / GAME_DAY;// 地球每秒转动的角度
private final static double SIN_HOUR_DEG = Math.sin(Math.PI / 12);// 日出一小时的高度
private long totalSec;// 游戏从开始到现在总共经过的秒数
private int year_sec;
private int month_sec;
private int date_sec;
private int hour_sec;
private int year;// 年份>=0
private int month;// 月份 [0~11]
private int date;// 日期 [0~29]
private int day;// 一年中的第几天[0~359]
private int hour;// 小时[0~23]
private int minute;// 分钟[0~59]
private double alpha;// 我们假设6点钟日出,α代表时针相对于6点钟的位置。
private double theta;// 我们假设一年每个月正午12点阳光的高度为θ
public GameDate() {
totalSec = 0l;
sunDirection = new Vector3f();
updateTime();
}
public GameDate(long lastTime) {
totalSec = lastTime;
sunDirection = new Vector3f();
updateTime();
}
public void update() {
totalSec++;
updateTime();
}
private void updateTime() {
年月日
year_sec = (int) (totalSec % GAME_YEAR);// 游戏中的一年过了多少秒。
year = (int) (totalSec / GAME_YEAR);// 经过了几年了?
day = year_sec / GAME_DAY;// 这是一年的第几天?
month_sec = year_sec % GAME_MONTH;// 游戏中一个月过了多少秒
month = year_sec/GAME_MONTH;// 经过了几个月了?
date_sec = month_sec % GAME_DAY;// 游戏中的一天过了多少秒
date = month_sec/GAME_DAY;// 经过了几天了?
/时分秒
hour_sec = date_sec % GAME_HOUR;// 游戏中的一小时过了多少秒
hour = date_sec / GAME_HOUR;// 今天过了几小时了?
minute = (int) (hour_sec / GAME_MINUTE);// 一小时过了几分钟了?
// 我们假设6点钟日出,α代表时针相对于6点钟的位置。
alpha = SEC_DEG * (date_sec - GAME_HOUR * 6);// 根据一天的时间,计算时钟的角度
updateDayAndNight();
theta = getTheta();// 根据地球公转的角度,计算日照高度。
updateSunDirection();
}
/**
* 游戏从开始到现在总共经过的秒数
* @return
*/
public long currentTimeInSecond() {
return totalSec;
}
/**
* 下面来计算太阳高度。太阳每天升起的高度都不一样,随地球公转而变化。
*/
public double getTheta() {
// 春分是春三月的中节,因此日期要回退45天
double year_angle = Math.PI * 2 * (day - 45) / 360;
// 世界的实际日照角度为
this.theta = WUHAN_LATITUDE + ECLIPTIC_OBLIQUITY * Math.sin(year_angle);
return theta;
}
private float lightPower;// 光照强度
private boolean isDay;// 是否是白天
public void updateDayAndNight() {
// 计算阳光强度
// 日出和日落时,太阳的亮度会渐变,当高度达到PI/6的时候,天就整个亮了。
// 让日出时间提前1个小时,让日落时间推后1个小时。
lightPower = (float) ((Math.sin(alpha) + SIN_HOUR_DEG) * 2);
if (lightPower > 1f)
lightPower = 1f;
if (lightPower < 0) {// 太阳落下了
lightPower = 0f;
if (isDay)
isDay = false;// 黑夜
} else {
if (!isDay)
isDay = true;// 白天
}
}
public float getLightPower() {
return lightPower;
}
public boolean isDay() {
return isDay;
}
private Vector3f sunDirection;// 光照角度
/**
* 下面来计算光照角度
*/
public void updateSunDirection() {
double x = -Math.cos(alpha);
double y = -Math.sin(alpha) * Math.sin(theta);
double z = -Math.sin(alpha) * Math.cos(theta);
sunDirection.set((float) x, (float) y, (float) z);
}
public Vector3f getSunDirection() {
return sunDirection;
}
public int getYear() {
return year;
}
public int getMonth() {
return month;
}
public int getDate() {
return date;
}
public int getHour() {
return hour;
}
public int getMinute() {
return minute;
}
}
然后我们写一个实例代码,来看看日出日落的效果。
注意:下例中所有模型,都来源于JME3自带的例子。
package org.pstale.utils;
import com.jme3.app.SimpleApplication;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Cylinder;
import com.jme3.shadow.BasicShadowRenderer;
import com.jme3.shadow.DirectionalLightShadowRenderer;
import com.jme3.texture.Texture.WrapMode;
import com.jme3.util.TangentBinormalGenerator;
/**
* 游戏时间以及昼夜系统测试
* @author yanmaoyuan
*
*/
public class TestGameDate extends SimpleApplication {
private GameDate gameDate;// 游戏时间
private DirectionalLight sunLight;// 太阳光
private BitmapText gui;// 用来显示游戏时间
private Geometry sunBox;// 用一个小方块来模拟代表太阳
@Override
public void simpleInitApp() {
// 初始化游戏时间
gameDate = new GameDate();
// 初始化镜头
cam.setLocation(new Vector3f(27.492603f, 29.138166f, -13.232513f));
cam.setRotation(new Quaternion(0.25168246f, -0.10547892f, 0.02760565f, 0.96164864f));
flyCam.setMoveSpeed(30);
// 我们创建一个红色小方块,用来代表太阳,它会根据时间的变化而移动。
Box box = new Box(5,5,5);
sunBox = new Geometry("Box", box);
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", ColorRGBA.Red);
sunBox.setMaterial(mat);
sunBox.setShadowMode(ShadowMode.Off);
rootNode.attachChild(sunBox);
setupGui();
setupLighting();
setupFloor();
setupSignpost();
}
/**
* 创建gui,用来显示时间
*/
public void setupGui() {
BitmapFont guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
gui = new BitmapText(guiFont, false);
gui.setText("00:00");
guiNode.attachChild(gui);
// 把gui放在屏幕顶部居中
float width = (settings.getWidth() - gui.getLineWidth())/2;
float height = settings.getHeight();
gui.setLocalTranslation(width, height, 0);
}
/**
* 创建光源
*/
public void setupLighting() {
// 阳光
sunLight = new DirectionalLight();
sunLight.setColor(ColorRGBA.White.clone());
sunLight.setDirection(gameDate.getSunDirection());
rootNode.addLight(sunLight);
// 设置一个很淡的环境光
AmbientLight al = new AmbientLight();
al.setColor(ColorRGBA.White.mult(0.3f));
rootNode.addLight(al);
rootNode.setShadowMode(ShadowMode.CastAndReceive);
// 阳光产生影子全靠这玩意了!
DirectionalLightShadowRenderer dlsr = new DirectionalLightShadowRenderer(assetManager, 512, 4);
dlsr.setLight(sunLight);
viewPort.addProcessor(dlsr);
}
/**
* 创建一个地板,这样我们才能看见影子。
*/
public void setupFloor() {
Material mat = assetManager.loadMaterial("Textures/Terrain/Pond/Pond.j3m");
mat.getTextureParam("DiffuseMap").getTextureValue().setWrap(WrapMode.Repeat);
mat.getTextureParam("NormalMap").getTextureValue().setWrap(WrapMode.Repeat);
mat.setBoolean("UseMaterialColors", true);
mat.setColor("Diffuse", ColorRGBA.White.clone());
mat.setColor("Ambient", ColorRGBA.White.clone());
// mat.setColor("Specular", ColorRGBA.White.clone());
// mat.getTextureParam("ParallaxMap").getTextureValue().setWrap(WrapMode.Repeat);
mat.setFloat("Shininess", 0);
// mat.setBoolean("VertexLighting", true);
Box floor = new Box(100, 1f, 100);
TangentBinormalGenerator.generate(floor);
floor.scaleTextureCoordinates(new Vector2f(5, 5));
Geometry floorGeom = new Geometry("Floor", floor);
floorGeom.setMaterial(mat);
floorGeom.setShadowMode(ShadowMode.Receive);// 地板只接受影子,不产生影子。
rootNode.attachChild(floorGeom);
}
/**
* 创建一个sign,我们可以看到它在阳光下的影子。
*/
public void setupSignpost() {
Spatial signpost = assetManager.loadModel("Models/Sign Post/Sign Post.mesh.xml");
Material mat = assetManager.loadMaterial("Models/Sign Post/Sign Post.j3m");
signpost.setMaterial(mat);
signpost.rotate(0, FastMath.HALF_PI, 0);
signpost.setLocalTranslation(0, 3.5f, 0);
signpost.setLocalScale(4);
signpost.setShadowMode(ShadowMode.CastAndReceive);
TangentBinormalGenerator.generate(signpost);
rootNode.attachChild(signpost);
}
@Override
public void simpleUpdate(float tpf) {
// 更新游戏时间
gameDate.update();
// 更新gui,显示当前时间
gui.setText(String.format("%02d:%02d", gameDate.getHour(), gameDate.getMinute()));
// 更新阳光亮度
float power = gameDate.getLightPower();
sunLight.setColor(ColorRGBA.White.clone().mult(power));
// 更新光照角度
sunLight.setDirection(gameDate.getSunDirection());
sunBox.setLocalTranslation(gameDate.getSunDirection().mult(-100f));
}
public static void main(String[] args) {
TestGameDate app = new TestGameDate();
app.start();
}
}