博客
关于我
从零开始实现放置游戏(十五)——实现战斗挂机(6)在线打怪练级
阅读量:433 次
发布时间:2019-03-06

本文共 17650 字,大约阅读时间需要 58 分钟。

  本章初步实现游戏的核心功能——战斗逻辑。
  战斗系统牵涉的范围非常广,比如前期人物的属性、怪物的配置等,都是在为战斗做铺垫。
  战斗中,人物可以施放魔法、技能,需要技能系统支持。
  战斗胜利后,进行经验、掉落结算。又需要背包、装备系统支持。装备系统又需要随机词缀附魔系统。
  可以说是本游戏最硬核的系统。
  因为目前技能、背包、装备系统都还没有实现。我们先初步设计实现一个简易战斗逻辑。
  战斗动作仅包括普通攻击,有可能产生未命中、闪避和暴击。
  整个战斗逻辑的流程大致如下图所示:
 

一、战斗消息设计

  参照其他消息,战斗动作需要发送请求并接收返回消息,我们先定义两个消息代码 :
    CBattleMob = "30003001"
    SBattleMob = "60003001"
 
  这里我们先仅考虑在线打怪,发送战斗请求,我们仅需要知道怪物id即可,战斗时从数据库读取怪物属性。
  新建客户端消息类如下:
@Datapublic final class CBattleMobMessage extends ClientMessage {    private String mobId;}
  服务端需要返回战斗的最终结果信息,以及每个回合每个角色的战斗动作记录作给客户端,一遍客户端播放。
  新建服务端的消息类如下:
@Datapublic class SBattleMobMessage extends ServerMessage {    private BattleMobResult battleMobResult;}
@Datapublic class BattleMobResult implements Serializable {    // 总回合数    private Integer totalRound;    // 回合列表    private List
roundList; // 是否玩家获胜 private Boolean playerWin; // 战斗结果信息 private String resultMessage; public BattleMobResult() { this.roundList = new ArrayList<>(); } public void putBattleRound(BattleRound battleRound) { this.roundList.add(battleRound); }}
@Datapublic class BattleRound implements Serializable {    // 当前回合数    private Integer number;    // 回合内战斗记录    private List
messages; // 是否战斗结束 private Boolean end; public BattleRound() { this.messages = new ArrayList<>(); this.end = false; } public BattleRound(Integer roundNum) { this(); this.number = roundNum; } public void putMessage(String message) { this.messages.add(message); }}
  这里 BattleMobResult 和 BattleRound 两个类,是返回给页面使用的视图模型,新建时放在game.hub.message.vo.battle包中。

二、战斗单位建模

  在战斗开始时,我们把参战单位那一时刻的属性取出来存一份副本,此后,均以此副本为准进行计算。
  怪物和玩家包的类含的属性差别较大,为了方便统一计算,我们抽象一个BattleUnit类,存储一些通用属性,比如等级,血量。
  其中还定义了一些抽象方法,比如获取攻击强度getAP(),和获取护甲等级getAC()。玩家和怪物需要分别实现这两个抽象方法。
  玩家,战斗属性(二级属性)是由力量、敏捷、耐力、智力这些一级属性进一步计算得出。比如,战士的攻击强度=等级*3+力量*2-20。速度=敏捷。护甲等级=耐力*2。命中率=0.95。闪避和暴击=敏捷*0.0005。
  怪物,只是用来练级的,则没那么麻烦,录入数据时就只有伤害和护甲两项属性。攻击强度直接取伤害值即可。速度直接取0。命中率默认0.95。闪避和暴击率默认0.05。
  这里虚类BattleUnit中又有一个巧妙的实方法getDR(),获取伤害减免。将其写在虚基类中,不管是玩家还是怪物实例,都可以根据自身的AC,计算出相应的DR。
  这里DR的计算公式: 伤害减免 = 护甲等级 / (护甲等级 + 85*玩家(怪物等级 + 400)
/**     * 获取伤害减免Damage Reduce     *     * @return     */    public Double getDR() {        Integer ac = this.getAC();        return ac / (ac + 85 * this.level + 400.0);    }
  3个类的UML图如下,具体实现可以下载源代码查看。

三、战斗机制

  模型建完,就剩战斗逻辑了。其中,一个核心的问题就是战斗动作的判定。即发起一次普通攻击后,到底是被闪避了,还是被格挡了,还是产生了暴击,或者仅仅是命中。其中,每一项可能的结果需要单独ROLL点吗?这里不同的游戏会有不同的实现。我们参考使用魔兽的判定方法,圆桌理论,即只ROLL一次点,这样逻辑更加容易处理。

  圆桌理论

  "一个圆桌的面积是固定的,如果几件物品已经占据了圆桌的所有面积时,其它的物品将无法再被摆上圆桌"
  这个理论在战斗逻辑中,即把可能产生的结果按优先级摆放到桌上,比如以下这种情形(其中的概率会因属性、装备等的不同而变化,这里只是举例):
  • 未命中(5%)
  • 躲闪(5%)
  • 招架(20%)
  • 格挡(20%)
  • 暴击(5%)
  • 普通攻击
  只ROLL一次点,如果ROLL到3,则玩家未命中怪物;如果ROLL到49,则玩家的攻击被怪物格挡;超过55的部分,都是普通攻击。
  假如这里玩家换上暴击装,暴击率达到60%。则圆桌上全部结果的概率已超出100%,ROLL到50-100全部判定为暴击,普通攻击被踢下圆桌,永远不会发生。
 
  在本此实现中,我们仅考虑物理未命中、闪避和暴击。暂不考虑二次ROLL点(攻击产生暴击,但被闪避或格挡了),以及法术技能的ROLL点。

四、战斗逻辑实现

  有了以上基础,我们就可以通过代码实现完整的战斗逻辑了。
  这里,虽然目前仅包含在线打怪,但以后可能会包含组队战斗,副本战斗,PVP等逻辑。我们把战斗逻辑放到单独的包里,com.idlewow.game.logic.battle,在这里新建战斗逻辑的核心类BattleCore,具体实现代码如下:
package com.idlewow.game.logic.battle;import com.idlewow.character.model.Character;import com.idlewow.character.model.LevelProp;import com.idlewow.character.service.CharacterService;import com.idlewow.character.service.LevelPropService;import com.idlewow.common.model.CommonResult;import com.idlewow.game.GameConst;import com.idlewow.game.logic.battle.dto.BattleMonster;import com.idlewow.game.logic.battle.dto.BattlePlayer;import com.idlewow.game.logic.battle.util.ExpUtil;import com.idlewow.game.hub.message.vo.battle.BattleMobResult;import com.idlewow.game.logic.battle.dto.BattleUnit;import com.idlewow.game.logic.battle.util.BattleUtil;import com.idlewow.game.hub.message.vo.battle.BattleRound;import com.idlewow.mob.model.MapMob;import com.idlewow.mob.service.MapMobService;import com.idlewow.support.util.CacheUtil;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.util.LinkedList;import java.util.List;import java.util.Random;@Componentpublic final class BattleCore {    private static final Logger logger = LogManager.getLogger(BattleCore.class);    // 战斗最大回合数    private static final Integer MaxRound = 20;    // 暴击系数    private static final Integer CriticalFactor = 2;    @Autowired    MapMobService mapMobService;    @Autowired    LevelPropService levelPropService;    @Autowired    CharacterService characterService;    /**     * 在线打怪     *     * @param character     * @param mobId     * @return     */    public BattleMobResult battleMapMob(Character character, String mobId) {        // 获取地图怪物信息        CommonResult commonResult = mapMobService.find(mobId);        if (!commonResult.isSuccess()) {            logger.error("未找到指定怪物:id" + mobId);            return null;        }        // 初始化参战方信息        MapMob mapMob = (MapMob) commonResult.getData();        List
atkList = new LinkedList<>(); atkList.add(this.getBattlePlayer(character, GameConst.BattleTeam.ATK)); List
defList = new LinkedList<>(); defList.add(this.getBattleMonster(mapMob, GameConst.BattleTeam.DEF)); List
battleList = new LinkedList<>(); battleList.addAll(atkList); battleList.addAll(defList); battleList = BattleUtil.sortUnitBySpeed(battleList); // 回合循环 BattleMobResult battleMobResult = new BattleMobResult(); for (int i = 0; i < MaxRound; i++) { BattleRound battleRound = new BattleRound(i + 1); for (BattleUnit battleUnit : battleList) { if (!battleUnit.getIsDefeat()) { // 选定攻击目标 BattleUnit targetUnit = null; if (battleUnit.getTeam().equals(GameConst.BattleTeam.ATK)) { Integer targetIndex = new Random().nextInt(defList.size()); targetUnit = defList.get(targetIndex); } else if (battleUnit.getTeam().equals(GameConst.BattleTeam.DEF)) { Integer targetIndex = new Random().nextInt(atkList.size()); targetUnit = atkList.get(targetIndex); } // 攻方出手ROLL点 Integer roll = new Random().nextInt(100); Double miss = (1 - battleUnit.getHitRate() / (battleUnit.getHitRate() + battleUnit.getDodgeRate())) * 100; Double critical = battleUnit.getCriticalRate() * 100; logger.info("round: " + i + "atk: " + battleUnit.getName() + " def: " + targetUnit.getName() + " roll:" + roll + " miss: " + miss + " cri: " + critical); String desc = ""; if (roll <= miss) { desc = battleUnit.getName() + " 的攻击未命中 " + targetUnit.getName(); } else if (roll <= miss + critical) { Integer damage = BattleUtil.actualDamage(battleUnit.getAP(), targetUnit.getDR()) * CriticalFactor; desc = battleUnit.getName() + " 的攻击暴击,对 " + targetUnit.getName() + " 造成 " + damage + " 点伤害(" + targetUnit.getHp() + " - " + damage + " )"; targetUnit.setHp(targetUnit.getHp() - damage); } else { Integer damage = BattleUtil.actualDamage(battleUnit.getAP(), targetUnit.getDR()); desc = battleUnit.getName() + " 的攻击,对 " + targetUnit.getName() + " 造成 " + damage + " 点伤害(" + targetUnit.getHp() + " - " + damage + " )"; targetUnit.setHp(targetUnit.getHp() - damage); } // 检测守方存活 if (targetUnit.getHp() <= 0) { targetUnit.setIsDefeat(true); desc += ", " + targetUnit.getName() + " 阵亡"; if (battleUnit.getTeam().equals(GameConst.BattleTeam.ATK)) { defList.remove(targetUnit); } else if (battleUnit.getTeam().equals(GameConst.BattleTeam.DEF)) { atkList.remove(targetUnit); } } else { // 检测守方反击动作 // todo } battleRound.putMessage(desc); // 检测战斗结束 if (atkList.size() == 0 || defList.size() == 0) { Boolean playerWin = defList.size() == 0; battleRound.setEnd(true); battleMobResult.setTotalRound(i); battleMobResult.setPlayerWin(playerWin); String resultMessage = "战斗结束! " + character.getName() + (playerWin ? " 获得胜利!" : " 不幸战败!"); battleMobResult.putBattleRound(battleRound); battleMobResult.setResultMessage(resultMessage); // 玩家获胜 进行战斗结算 if (playerWin) { // 经验结算 this.settleExp(character.getLevel(), mapMob.getLevel(), character); // 更新角色数据 characterService.updateSettle(character); } return battleMobResult; } } } battleMobResult.putBattleRound(battleRound); } battleMobResult.setTotalRound(MaxRound); battleMobResult.setResultMessage("战斗回合数已用尽!守方获胜!"); return battleMobResult; } /** * 经验值结算 * @param charLevel 角色等级 * @param mobLevel 怪物等级 * @param character 角色信息 */ private void settleExp(Integer charLevel, Integer mobLevel, Character character) { Integer exp = ExpUtil.getBattleMobExp(charLevel, mobLevel); if (exp > 0) { Integer levelUpExp = CacheUtil.getLevelExp(charLevel); if (character.getExperience() + exp >= levelUpExp) { character.setLevel(charLevel + 1); character.setExperience(character.getExperience() + exp - levelUpExp); } else { character.setExperience(character.getExperience() + exp); } } } /** * 获取角色战斗状态 * @param character 角色信息 * @param battleTeam 所属队伍 * @return */ private BattlePlayer getBattlePlayer(Character character, String battleTeam) { LevelProp levelProp = levelPropService.findByJobAndLevel(character.getJob(), character.getLevel()); BattlePlayer battlePlayer = new BattlePlayer(); battlePlayer.setId(character.getId()); battlePlayer.setName(character.getName()); battlePlayer.setJob(character.getJob()); battlePlayer.setLevel(character.getLevel()); battlePlayer.setHp(levelProp.getHp()); battlePlayer.setStrength(levelProp.getStrength()); battlePlayer.setStamina(levelProp.getStamina()); battlePlayer.setAgility(levelProp.getAgility()); battlePlayer.setIntellect(levelProp.getIntellect()); battlePlayer.setTeam(battleTeam); return battlePlayer; } /** * 获取怪物战斗状态 * @param mapMob 怪物信息 * @param battleTeam 所属队伍 * @return */ private BattleMonster getBattleMonster(MapMob mapMob, String battleTeam) { BattleMonster battleMonster = new BattleMonster(); battleMonster.setId(mapMob.getId()); battleMonster.setName(mapMob.getName()); battleMonster.setLevel(mapMob.getLevel()); battleMonster.setHp(mapMob.getHp()); battleMonster.setDamage(mapMob.getDamage()); battleMonster.setArmour(mapMob.getArmour()); battleMonster.setTeam(battleTeam); return battleMonster; }}
BattleCore.java
  如上图代码,首先我们初始化一份各参战单位的属性副本,并添加到创建的3个列表中,其中atkList, defList用来检测是否其中一方全部阵亡,battleList则用来对参战单位按速度排序,确定出手顺序。
  这里使用了归并排序来对集合进行排序,具体算法在BattleUtil类中。考虑到这里对集合的添加、修改、删除操作较多,使用LinkedList链表来保存参战集合。(实际上数据较少,使用ArrayList可能也没什么差别)。
  这里仅仅在回合开始前确定了一次出手顺序,因为目前没有引入技能,假如引入技能后,比如猎人施放豹群守护,我方全体速度+50,那么需要对出手列表进行重新排序。
  进入循环后,随机选定攻击目标 --> 确定出手动作 --> 存活检测 --> 战斗结束检测, 这里注释和代码比较清楚,就不一一讲解了。
  这里攻击动作和结果确定后,会在返回信息中添加对此的描述,后面考虑如果后端传输这些内容太多不够优雅,也可以定义一套规则,只传输关键数据,战斗记录由前端生成。不过目前先不考虑。
  战斗结束后,如果玩家胜利,需要结算经验值。经验值相关的计算在ExpUtil中,文后会附上经验值计算公式。

五、播放战斗记录

  战斗计算完成后,后端会返回战斗信息给前端,前端只负责播放即可。
  播放记录的方法代码如下:
 
// 加载在线打怪战况    loadBattleMobResult: async function (data) {        let that = this;        $('.msg-battle').html('');        let rounds = data.battleMobResult.roundList;        if (data.battleMobResult.totalRound > 0) {            for (let i = 0; i < rounds.length; i++) {                let round = rounds[i];                let content = "

【第" + round.number + "回合】

"; for (let j = 0; j < round.messages.length; j++) { content += "

" + round.messages[j] + "

"; } content += "

"; $('.msg-battle').append(content); await this.sleep(1500); } $('.msg-battle').append("

" + data.battleMobResult.resultMessage + "

"); if (data.battleMobResult.playerWin) { that.sendLoadCharacter(); } if (that.isBattle) { that.battleInterval = setTimeout(function () { that.sendBattleMob(that.battleMobId); }, 5000); } // await this.sleep(5000).then(function () { // that.sendBattleMob(data.battleMobResult.mobId); // }); } },
  上面的代码中,最后3行被注释掉的代码,即5秒钟后,再次攻击此怪。如果只考虑打怪和用setTimeout方法实现,其实没有差别。
  但在业务上,考虑玩家可能需要点击停止打怪,那么用setTimeout来执行循环,可以用clearInterval来终止函数执行。
/* 在线打怪 */function battleMob(mobId) {    let diff = new Date().getTime() - wowClient.battleMobTime;    if (diff < TimeLag.BattleMob * 1000) {        let waitSeconds = parseInt(TimeLag.BattleMob - diff / 1000);        alert('请勿频繁点击!' + waitSeconds + '秒后可再操作!');        return;    }    if (mobId === wowClient.battleMobId) {        alert("已经在战斗中!请勿重复点击!");        return;    }    wowClient.battleMobId = mobId;    wowClient.battleMobTime = new Date().getTime();    if (!wowClient.isBattle) {        wowClient.isBattle = true;        wowClient.sendBattleMob(mobId);    }}
  上图中是点击‘打怪’按钮的方法,这里我直接把代码贴出来,显得比较清晰简单。实际上做的时候,经过反复改动和考虑。代码中解决的一些问题,可能三言两语也不太好体现出来,需要自己实际编写代码才能体会。
  比如考虑这个场景,玩家A,在线攻击怪物a , 开启的对a的战斗循环。A升级后,想攻击更高级的怪物b。这时比较合理的操作方式就是玩家直接点击b的战斗按钮。
  那么我们可能要考虑几个问题:
    怪物a的战斗循环需不需要停止,怎么停止;如果要停止战斗,但此时正在播放战斗记录,还没进入5秒的循环,停止循环函数不会生效,该怎么办;播放中对a的战斗记录需不需要立即清除;对b的战斗需不需点击后立即开始。。。
  起初我是按照两条线程的思路来进行实现,即a的线程仍在进行,建立标志位将其停止,点击后立即开启b的线程,但实现起来非常复杂,而且有些问题不好解决,比如a的战斗记录没播放完,b已经发送了战斗请求,那么就需要停止播放a的记录,并清屏,开始播放b的战斗记录。
  后来发现,只需要一个线程即可。仅需要标记战斗目标的怪物id,战斗线程仅对标记的怪物id发送战斗请求,切换战斗目标后,因为被标记的怪物id已经变了,所以a的战斗记录播放完毕后,5秒后自动请求战斗的怪物id已变成了b,这样自动切换到了对b的战斗。从页面表现上,也更符合逻辑。

F&Q

  Q.在初始化战斗时,为什么要把玩家和怪物放到列表中?
  A.考虑后面会有组队战斗。以及战斗技能,比如法师召唤水元素,猎人带宠物。虽然目前仅是1v1,但实现时作为队伍来考虑更方便扩展。
  
  Q.为什么角色阵亡后,仅把其从攻方(守方)列表中移除,不从全体出手列表中移除?
  A.考虑到牧师,骑士可以施放复活技能,阵亡后的角色仍保留在列表中,对性能影响不大,方便以后技能的实现。

附-经验值计算

艾泽拉斯的怪物经验公式是 45+等级*5外域的怪物经验公式是 235+等级*5基础知识魔兽里你选取怪物以后一般名字级别上面有颜色,它指示你和怪物之间的等级差别,骷髅级别 怪物级别大于等于玩家级别的10级红色 怪物级别大于等于玩家级别的5级橙色 怪物级别大于等于玩家级别的3或者4级黄色 怪物级别小于等于玩家级别2级和大于大于等于玩家级别2级之间绿色 怪物级别小于玩家级别3级,但是还未变灰灰色 玩家级别1-5级: 灰色级别小于等于0(没有灰色的怪)玩家级别 6-39级:灰色级别小于等于 玩家级别-(玩家级别÷10取整数上限) -5玩家级别 40-59级:灰色级别小于等于 玩家级别-(玩家级别÷5取整数上限) -1玩家级别 60-70级:灰色级别小于等于 玩家级别-9注:整数上限是指不小于该值的最小整数,比如4.2整数上限是5,3.0整数上限是3单杀怪的经验杀死灰色级别的怪是没有经验的,其他颜色级别的怪单杀的经验值计算如下:(艾泽拉斯)相同级别的怪:经验=(玩家等级×5+45)高等级怪:经验=(玩家等级×5+45)×(1+0.05×(怪物等级-玩家等级) ,当怪物等级大于玩家等级4级以上均按4级计算,哪怕精英怪低等级怪:有一个零差值系数ZD (zero difference value)ZD = 5, when Char Level = 1 - 7ZD = 6, when Char Level = 8 - 9ZD = 7, when Char Level = 10 - 11ZD = 8, when Char Level = 12 - 15ZD = 9, when Char Level = 16 - 19ZD = 11, when Char Level = 20 - 29ZD = 12, when Char Level = 30 - 39ZD = 13, when Char Level = 40 - 44ZD = 14, when Char Level = 45 - 49ZD = 15, when Char Level = 50 - 54ZD = 16, when Char Level = 55 - 59ZD = 17, when Char Level = 60+经验=(玩家等级×5+45)×(1-(玩家等级-怪物等级)÷ 零差值系数)计算的例子如下:假设玩家等级 = 20.那么灰名怪物等级 = 13, 根据以上表格获得.杀掉任何 13 级或者以下的怪得不到经验。同等级基础经验为 (20 * 5 + 45) = 145. 杀掉一个 20 级别的怪将能获得145点经验。对于一个 21级怪, 你将获得 145 * (1 + 0.05 * 1) = 152.2 四舍五入为 152 点经验.根据上面表格ZD值是11。对于18级的怪, 我们将有 145 * (1 - 2/11) = 118.6 四舍五入为 119点经验。对于16级的怪, 我们将有 145 * (1 - 4/11) = 92.3四舍五入为 92点经验.对于14级的怪, 我们将有 145 * (1 - 6/11) = 65.91四舍五入为 66点经验.对于燃烧的远征外域的怪,经验计算较多,笔者根据表格值推论公式如下:相同级别的怪:经验=(玩家等级×5+235)高等级怪:经验=(玩家等级×5+235)×(1+0.05×(怪物等级-玩家等级) ,当怪物等级大于玩家等级4级以上均按4级计算,哪怕精英怪。低等级怪:经验=(玩家等级×5+235)×(1-(玩家等级-怪物等级)÷ 零差值系数)精英怪经验=普通同等级怪经验×2精力充沛时间经验=普通计算经验×2 (耗尽精力充沛点数为止,故而最后一个精力充沛期间杀的怪未必能达到经验×2)影响杀怪经验的因素对于大号带小号或者抢怪的情况,玩家杀怪的经验值就会有变化。一般来说,原则如下:如果你开怪并造成了伤害,那么怪物就是你的;这个时候,如果有别的玩家或者大号来杀了这个怪,那么如果帮助杀怪的人对于这个级别的怪他能够获得经验,则属于抢怪,不会影响你的经验获得;如果帮助你杀怪的人对于这个级别的怪或不得经验,那么就是就属于带小号了,你获得很少很少的经验,非常的不划算。因此,对于一个60级的玩家来带小号,不管什么级别的怪,他都没有经验(TBC以前),所以小号获得的经验非常少!而如果是59的玩家帮忙杀50+的怪,那么经验都是小号的!战斗中如果有别人帮你加血,加血只会扣掉你很少的经验,用大号跟随加血小号练级是不错的办法;别人给你加的伤害护盾(比如说荆棘术什么的)只会影响你非常少的经验,5-10点最坏情况,基本可以无视了,放心的加吧。综上所述,用不满级的相同等级区间里的号带小号效率最高,比如49的带40的,59的带50的…… 但是,大号基本都是60的,没办法,呵呵,只能帮带任务或者副本了。组队经验值根据wiki的资料,这个只是推论,未必精确假设一个队伍中的人都是同等级的,那么每个人的经验=单杀怪的经验÷人数×系数系数是:1人:12人:13人:1.1664人:1.35人:1.4例子如下:杀100经验的怪1人 = 100xp2人 = 50xp 每人.3人 = ~39xp 每人.4人 = ~33xp 每人.5人 = ~28xp 每人.两人队伍计算公式假设 玩家1级别>玩家2级别那么 基础经验按玩家1级别计算最后分得的经验玩家1获得 基础经验×玩家1级别÷(玩家1级别+玩家2级别)玩家1获得 基础经验×玩家2级别÷(玩家1级别+玩家2级别)团队里面经验值要打折(除以2)
经验值计算

效果演示

本章小结

  注意,之前数据库和模型有个列名的单词写错了,我在源码中修正了。
  即map_mob的护甲字段,应为armour,之前写成了amour。如需运行源码,请先修正数据库中的列名。
 
  至此,游戏最重要的战斗功能已有了。
  后面可以开始逐步扩展背包,装备,掉落,随机附魔等重要功能。
 
  本文原文地址:
  本章源码下载地址:
  项目交流群:329989095
  
demo 演示地址:
   (服务器到期啦,等有时间搭了新服务器再更新这里)
创建角色时,请选择 人类 - 战士, 因为其他种族和职业的数值没有配置。

转载地址:http://zwmyz.baihongyu.com/

你可能感兴趣的文章
IntelliJ IDEA 中,项目文件右键菜单没有svn选项解决办法
查看>>
IDEA 调试Java代码的两个技巧
查看>>
微软XAML Studio - WPF, Sliverlight, Xamarin, UWP等技术开发者的福音
查看>>
深入理解JavaScript函数
查看>>
(在模仿中精进数据可视化07)星球研究所大坝分布可视化
查看>>
(数据科学学习手札27)sklearn数据集分割方法汇总
查看>>
从零开始学安全(十六)● Linux vim命令
查看>>
阿里巴巴Json工具-Fastjson教程
查看>>
Spring Cloud Gateway - 快速开始
查看>>
Java对象转JSON时如何动态的增删改查属性
查看>>
Python 面向对象进阶
查看>>
Linux常用统计命令之wc
查看>>
shell脚本里使用echo输出颜色
查看>>
并发编程——IO模型详解
查看>>
Java之封装,继承,多态
查看>>
wait()与notify()
查看>>
使用js打印时去除页眉页脚
查看>>
Spring security OAuth2.0认证授权学习第二天(基础概念-RBAC)
查看>>
ORA-00904: "FILED_TYPE": 标识符无效
查看>>
MapReduce实验
查看>>